Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "skillhook",
"displayName": "skillhook",
"version": "0.5.0",
"version": "0.6.0",
"description": "Turn this machine into a permanent, secure webhook endpoint that runs Agent Skills with Claude Code or Codex. Two skills teach the agent to install and expose skillhook and to write good webhook skills; the bundled MCP server manages skills, secrets, jobs and exposure.",
"author": {
"name": "Meter",
Expand Down
2 changes: 1 addition & 1 deletion .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "skillhook",
"version": "0.5.0",
"version": "0.6.0",
"description": "Turn this machine into a permanent, secure webhook endpoint that runs Agent Skills with Claude Code or Codex. Two skills teach the agent to install and expose skillhook and to write good webhook skills; the bundled MCP server manages skills, secrets, jobs and exposure.",
"author": {
"name": "Meter",
Expand Down
2 changes: 1 addition & 1 deletion .cursor-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "skillhook",
"version": "0.5.0",
"version": "0.6.0",
"description": "Turn this machine into a permanent, secure webhook endpoint that runs Agent Skills with Claude Code or Codex. Two skills teach the agent to install and expose skillhook and to write good webhook skills; the bundled MCP server manages skills, secrets, jobs and exposure.",
"author": {
"name": "Meter",
Expand Down
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ is `skillhook`. User docs: `README.md`, `docs/`, `llms.txt`.
| `src/events.ts` | The in-process event bus (`Events`, `EventMap`): the queue publishes `job.*`, the scheduler `schedule.*`, the registry `skill.changed`, `serve` `server.*`; `GET /events` and `GET /jobs/<id>/events` stream it (SSE, `openEventStream` in `src/server.ts`). The cloud link will subscribe to the same bus. |
| `src/progress.ts`, `src/answer.ts`, `src/mcp-job.ts`, `src/commands/job.ts` | The job API for the running agent and the human loop. `progress.ts` is the file model in the job directory (`progress.jsonl`, `progress.json`, `question.json`, `answer.json`) that the queue watches; `mcp-job.ts` serves it as the per-run MCP server (`skillhook mcp --job`, injected by the runners) and `commands/job.ts` as `skillhook job progress\|ask\|outcome\|note\|context`; `answer.ts` (leaf, like `manual.ts`) delivers a person's answer live or as a `trigger: resume` job that reopens the session. |
| `src/runners/` | `claude.ts`, `codex.ts`, `shell.ts`: build argv, parse output; `env.ts` is the env allow-list (`baseRunEnv` is also what probes run with); `failure.ts` classifies a failed run (`failure.kind`, from the CLIs' captured lines) and holds the `fallback` / `retry` schemas. |
| `src/cloud/` | The Skillhook Cloud side of this machine (`control.ts`: the commands that act on it, with the config keys the cloud may never change; `seal.ts`: X25519 + AES-GCM sealing for secrets). `protocol.ts` is the wire protocol as pure zod (no `node:` imports; exported as `@meterapp/skillhook/protocol`, the cloud repo imports it), with the vocabulary repeated as literals and a drift test; `config.ts` holds the URL rules, the kill switch and `commandAllowed`; `link.ts` is the sync loop `serve` runs (idle until `cloud.enabled`), `outbox.ts` the spool and ledgers in `jobs/.cloud/`, `redact.ts` what is removed before anything leaves, `commands.ts` the command dispatcher, `ingress.ts` hosted deliveries replayed to the local server, `pair.ts` / `src/commands/cloud.ts` pairing. Tests talk to `src/test-support/fake-cloud.ts`, never to a real cloud. |
| `src/cloud/` | The Skillhook Cloud side of this machine (`control.ts`: the commands that act on it, with the config keys the cloud may never change; `seal.ts`: X25519 + AES-GCM sealing for secrets). `protocol.ts` is the wire protocol as pure zod (no `node:` imports; exported as `@meterapp/skillhook/protocol`, the cloud repo imports it), with the vocabulary repeated as literals and a drift test; `config.ts` holds the URL rules, the kill switch and `commandAllowed`; `link.ts` is the sync loop `serve` runs (idle until `cloud.enabled`), `outbox.ts` the spool and ledgers in `jobs/.cloud/`, `redact.ts` what is removed before anything leaves, `commands.ts` the command dispatcher, `ingress.ts` hosted deliveries replayed to the local server, `pair.ts` / `src/commands/cloud.ts` pairing, `report.ts` a person's problem report (`cloud report`, MCP `cloud_report_issue`) with the machine's diagnostics, `api.ts` the fleet reads with an organisation API key (`cloud login|machines|jobs|job`). Tests talk to `src/test-support/fake-cloud.ts`, never to a real cloud (and `fake-server.ts` stands in for a running server). |
| `src/stats.ts` | Pure aggregation over job records and delivery records (`computeStats`) and `collectStats` over the store and the log: `GET /stats`, `skillhook stats`, MCP `get_stats`. New numbers go here with a unit test on synthetic records. |
| `src/readiness.ts` | Is a runner installed and logged in (`checkReadiness`, `ReadinessCache`): the queue's pre-flight before every job, `GET /runners`, `skillhook runners`, `runners.changed`. A not-ready runner fails the job fast or hands it to a `fallback:` runner; a failed run may be retried or handed over only before the agent produced anything. |
| `src/ops.ts` | Shared operations (create skill, run locally, sign+send, resolve URLs). CLI and MCP both call this; do not duplicate logic in either. |
Expand Down Expand Up @@ -62,7 +62,7 @@ Runtime state lives outside the repo in `~/.skillhook` (`SKILLHOOK_HOME`):
- **Every CLI command supports `--json`** and returns non-zero on failure. Register new commands in `COMMANDS` and `HELP` in `src/commands/main.ts`, then in the README table.
- **Third-party facts** (Granola, Sentry, GitHub, Tailscale) are stated in `docs/` and the examples with the exact header names; change them only with a source.
- **Tests are hermetic**: `tempHome()` from `src/test-support/helpers.ts`, fake runners, ephemeral ports. Never touch `~/.skillhook`, the real `claude`/`codex`, `launchctl` or `tailscale` from a test. Never reach the real npm registry either: point `SKILLHOOK_NPM_REGISTRY` at a local `node:http` server or set `SKILLHOOK_NO_UPDATE_CHECK=1`.
- **Outbound requests are opt-in and enumerated.** By default the CLI phones home once a day, and only for the update check (`src/update.ts`: the registry's `latest` dist-tag, cached 24 h, never on `--json`, in CI, or when `SKILLHOOK_NO_UPDATE_CHECK` / `update_check: false` say so). The one other outbound connection is the Skillhook Cloud link (`src/cloud/link.ts`), and only after `skillhook cloud connect` wrote `cloud.enabled` and `SKILLHOOK_CLOUD_TOKEN`: it talks to `cloud.url` over HTTPS only, sends only what `docs/cloud.md` lists (redacted, scrubbed of every `.env` value), obeys `cloud.mode` and the local allow/deny lists (which the cloud cannot change), and stops on `cloud.enabled: false`, `SKILLHOOK_NO_CLOUD=1` or `cloud disconnect`. Never enable it by default, from `init` or from a job; do not add other outbound requests the user did not ask for, and never auto-install anything.
- **Outbound requests are opt-in and enumerated.** By default the CLI phones home once a day, and only for the update check (`src/update.ts`: the registry's `latest` dist-tag, cached 24 h, never on `--json`, in CI, or when `SKILLHOOK_NO_UPDATE_CHECK` / `update_check: false` say so). The one other outbound connection is the Skillhook Cloud link (`src/cloud/link.ts`), and only after `skillhook cloud connect` wrote `cloud.enabled` and `SKILLHOOK_CLOUD_TOKEN`: it talks to `cloud.url` over HTTPS only, sends only what `docs/cloud.md` lists (redacted, scrubbed of every `.env` value), obeys `cloud.mode` and the local allow/deny lists (which the cloud cannot change), and stops on `cloud.enabled: false`, `SKILLHOOK_NO_CLOUD=1` or `cloud disconnect`. Never enable it by default, from `init` or from a job. Besides the link, the person-invoked cloud commands send one request each, only when run, only to the machine's cloud URL (`cloud.url` or `SKILLHOOK_CLOUD_URL`, HTTPS only): `cloud connect` / `disconnect` (pairing, revocation), `cloud report` and the MCP tool `cloud_report_issue` (`src/cloud/report.ts`: the person's text plus the diagnostics `docs/cloud.md` lists, scrubbed like the link's uploads, with the machine token), and `cloud login|machines|jobs|job` (`src/cloud/api.ts`: reads with the person's organisation API key `SKILLHOOK_CLOUD_API_KEY`, never with the machine token); the last two groups refuse under `SKILLHOOK_NO_CLOUD=1`. Do not add other outbound requests the user did not ask for, and never auto-install anything.

## Checks

Expand Down
33 changes: 33 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,39 @@ All notable changes to skillhook, newest first. The format follows [Keep a Chang

## Unreleased

## 0.6.0 (2026-09-29)

- `skillhook cloud report "<title>"`: a person on a paired machine reports a problem to the Skillhook
team without leaving the terminal (`--body TEXT`, `--body -` or `--body-file PATH`, `--kind`,
`--severity`, `--job`, `--delivery`, `--skill`, `--email`). One request, `POST /api/agent/issues`
with the machine token, carrying the person's text and, unless `--no-diagnostics`, what the machine
already knows: skillhook, Node, OS and architecture, `cloud.mode`, the link's state, whether each
runner is ready and the health summary with the failing and warning checks. Every `.env` value is
scrubbed from all of it, and payloads, logs, prompts and job output never go; `--dry-run` prints the
exact JSON instead of sending it. It answers `Reported as #N: <url>` and whether a confirmation email
went out. Each report carries a `report_id` (a UUID) on every attempt: network errors, timeouts and
5xx are retried (three attempts), a 429 only after a short `retry_after_ms`, another 4xx never, and
the cloud files a retried report once. The MCP tool `cloud_report_issue` sends the same report for
an agent the person asked (`dry_run: true` returns it without sending). Refused on a machine that is
not paired and under `SKILLHOOK_NO_CLOUD=1`.
- The protocol gains the report, additively (`PROTOCOL_VERSION` stays 1): `IssueReportRequestSchema`
(with `report_id`, a client-generated idempotency key), `IssueReportResponseSchema`,
`IssueDiagnosticsSchema`, `ISSUE_KINDS`, `ISSUE_SEVERITIES` and `LIMITS.max_issue_report_bytes`
(64 KiB), documented in [docs/cloud-protocol.md](docs/cloud-protocol.md#issue-reports).
- The organisation's fleet from the CLI with an organisation API key: `skillhook cloud login --key
shc_…|-` checks the key (`GET /api/v1/me`) and keeps it in `.env` as `SKILLHOOK_CLOUD_API_KEY`
without ever printing it (the environment variable works too, for CI), `cloud logout` forgets it,
and `cloud machines`, `cloud jobs [--machine M] [--skill S] [--status ST] [--outcome O] [--waiting]
[--limit N] [--before C]` and `cloud job <id>` print tables, or the API's JSON with `--json`. The
machine token is never used for them, so a paired machine cannot read the rest of its organisation.
A refused key says to log in again, a missing scope names it. A key only goes to a cloud URL someone
set (pairing, `cloud.url` or `SKILLHOOK_CLOUD_URL`), never to the built-in placeholder, and only if
it looks like one (`shc_…`, one line).
- The cloud HTTP client reads the public API's RFC 9457 problem answers (`code`, `detail`,
`request_id`) as well as the agent API's `error` / `message`, refuses a token that is not one line
of printable characters before it becomes a header, and never lets a token into an error message.
- `skillhook cloud <subcommand> --help` prints the usage instead of running the subcommand.

## 0.5.0 (2026-09-28)

- The cloud link checks the runners as soon as it connects, so the dashboard shows whether `claude` and
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ Give everything that has a trigger a webhook. Anything that can call a URL can s
- Any skill or hook can also carry a `schedule:` (a cron expression, a time zone, and what to do about missed slots); the server fires it without a webhook. See [Scheduled hooks](#scheduled-hooks).
- The runner is the real `claude` or `codex` CLI on the machine, so subscriptions, MCP servers, `CLAUDE.md`/`AGENTS.md` files and tool permissions apply as usual.
- Responses are immediate (`202` with a job id) or synchronous with `?wait=N` (or `Prefer: wait=N`); the agent's final message becomes the job result, and what it reports in `response.json` (`completed`, `partial`, `needs_human`, `nothing_to_do`, `failed`) becomes the job's `outcome`, so `skillhook jobs list --outcome needs_human` shows what is waiting for a person.
- Optional: `skillhook cloud connect` pairs the machine with Skillhook Cloud, a hosted dashboard for every machine's webhooks, jobs, questions waiting for a person, health and stats, with hosted webhook URLs that keep deliveries while the machine sleeps. Opt-in, outbound only, observe mode unless you choose control. See [docs/cloud.md](docs/cloud.md).
- Optional: `skillhook cloud connect` pairs the machine with Skillhook Cloud, a hosted dashboard for every machine's webhooks, jobs, questions waiting for a person, health and stats, with hosted webhook URLs that keep deliveries while the machine sleeps. Opt-in, outbound only, observe mode unless you choose control. `skillhook cloud report` sends the Skillhook team a problem report from a paired machine; `skillhook cloud machines` and `cloud jobs` read the whole fleet with an organisation API key. See [docs/cloud.md](docs/cloud.md).
- The agent is not cut off while it runs: a per-run job API (MCP tools injected into the run, or `skillhook job …`) lets it report progress and ask a person a question; `skillhook jobs answer <id> "…"` delivers the answer to the waiting agent or, when the run already ended, starts a new job that resumes the Claude or Codex session with it. See [Reporting progress and asking a person](docs/skills.md#reporting-progress-and-asking-a-person).
- Developed against Claude Code 2.1.270, Codex CLI 0.153.4 and Tailscale 1.102.3. skillhook drives the CLIs through their headless flags (`claude -p --output-format stream-json …`, `codex exec --json …`; `response: { mode: structured }` adds `claude --json-schema` / `codex --output-schema`); `skillhook run <skill> --dry-run` shows the exact command line.

Expand Down Expand Up @@ -336,6 +336,8 @@ Agents reading this repository should start with [`AGENTS.md`](AGENTS.md) (layou
| `skillhook deliveries list [--skill S] [--outcome O] [--since ISO] [--after ID] [--limit N]` · `deliveries show <id> [--body]` · `deliveries replay <id> [--force] [--skip-filters] [--wait S]` | Every webhook the server received, whatever became of it: accepted, duplicate, in flight, skipped by a filter, rejected (with the status and reason), Slack challenge; replay one through the skill as it is now. |
| `skillhook mcp [--print-config]` · `mcp --job` | MCP server over stdio; `--print-config` prints client configuration; `--job` serves one run's job API (the runners start it). |
| `skillhook cloud connect --code XXXX-XXXX [--control] [--url U]` · `cloud status` · `cloud disconnect [--keep-token]` | Pair this machine with Skillhook Cloud (opt-in, outbound only; observe mode unless `--control`): webhooks, jobs, health and stats of every machine in one place, hosted webhook URLs. See [docs/cloud.md](docs/cloud.md). |
| `skillhook cloud report "<title>" [--body T\|--body-file F\|--body -] [--kind bug\|question\|feature\|other] [--severity low\|normal\|high\|urgent] [--job ID] [--delivery ID] [--skill S] [--email E] [--no-diagnostics] [--dry-run]` | Report a problem to the Skillhook team from a paired machine, with the diagnostics it already has (versions, link state, runner readiness, failing checks; scrubbed of every `.env` value, never payloads or logs; `--dry-run` shows the JSON). See [docs/cloud.md](docs/cloud.md#reporting-a-problem). |
| `skillhook cloud login --key shc_…\|-` · `cloud logout` · `cloud machines` · `cloud jobs [--machine M] [--skill S] [--status ST] [--outcome O] [--waiting] [--limit N] [--before C]` · `cloud job <id>` | Read the organisation's machines and jobs with an organisation API key (kept in `.env` as `SKILLHOOK_CLOUD_API_KEY`, never printed; never the machine token). See [docs/cloud.md](docs/cloud.md#reading-the-fleet-with-an-api-key). |
| `skillhook config show\|get <key>\|set <key> <value>\|unset <key>\|reload\|path` | Read and edit `skillhook.json`; `set`/`unset` tell the running server, which applies every key but `host` and `port` live. |
| `skillhook link [dir] [--no-secret]` / `skillhook unlink <dir>` | Serve the hooks a repository declares in its `skillhook.yaml` (default `.`); stop serving them. |
| `skillhook projects [list]` / `skillhook projects init [dir] [--force]` | List linked repositories and their hooks; write a starter `skillhook.yaml` and link it. |
Expand Down
Loading
Loading