Skip to content

Skillhook Cloud from the terminal and the agent: the cloud's tools and the skillhook-cloud MCP server (0.7.0) - #17

Merged
JOsacky merged 7 commits into
mainfrom
claude/cloud-toolkit
Oct 2, 2026
Merged

JOsacky merged 7 commits into
mainfrom
claude/cloud-toolkit

Conversation

@JOsacky

@JOsacky JOsacky commented Oct 2, 2026 •

Copy link
Copy Markdown
Member

What

Everything the Skillhook Cloud dashboard shows and does, from the terminal and for an agent, with an organisation API key. The cloud now publishes its tools (GET /api/v1/tools: each with its JSON Schema and the scope it needs; MeterApp/skillhook-cloud#18), and skillhook builds its commands and MCP tools from that list at run time: a tool the cloud adds works here without a new skillhook, and no cloud tool is hand-written in this repo.

CLI

  • skillhook cloud overview: what needs a person across the organisation (agents waiting with their question, open alerts, failing checks with the fix, failed jobs and rejected webhooks of 24 h, the day's numbers, next steps).
  • skillhook cloud tools [tool]: what this key can do (and what a wider key would add); one tool's parameters.
  • skillhook cloud <tool> [args] [--param value]…: any tool by name (answer_job <job> "yes" --option yes, run_skill mac-mini triage --payload @event.json --wait-seconds 120, get_stats --days 30, save_skill mac-mini triage --content-file SKILL.md, …). The words after the tool's name are read with its schema (before it, only skillhook's own options; a switch never swallows the next word, a text parameter takes it; --x=value, --no-x, --); required parameters as arguments in order, checked numbers, JSON as a literal, @file or -, --param-file for long text, --input for the whole input. skillhook's own options (--json, --help/-h, --version/-v, --dir) mean the same wherever they stand and are never a parameter's value, so --help never runs a tool and --answer=--help sends that text. Text output without control characters or bidirectional overrides (errors included), or --json (where they are \u escapes, for every command). A tool beyond the key's scope is refused before anything is sent.
  • skillhook cloud secret <machine> <skill|NAME> [--force]: the machine generates a skill's secret (only a skill's: never its admin token or a runner's key) sealed to a one-time X25519 key pair here; only this terminal opens it, the cloud only forwards the sealed value.
  • skillhook cloud login [--url URL] checks the key against the cloud named (else the one logged in to before, else the machine's; when those two differ it asks which) and keeps it bound to that cloud (SKILLHOOK_CLOUD_API_URL in .env): the key never goes anywhere else, whatever cloud.url says later, and login never touches the machine's link. The same key from the environment goes there too; a key kept by 0.6 goes to the machine's cloud until the next login, and cloud status says so. At a terminal it asks for the key without echo. cloud status / MCP cloud_status say whether a key is kept, for which cloud.
  • skillhook's own tools do not move the link or the key for an agent: the local MCP server refuses cloud.* in update_config and SKILLHOOK_CLOUD_* in set_secret / generate_secret, and a skill's secret_env can no longer be SKILLHOOK_ADMIN_TOKEN or SKILLHOOK_CLOUD_* (the machine also refuses the cloud's secret.generate / secret.set for those, by whatever name they are asked for). An agent that runs commands on the machine acts with the person's rights all the same; the docs say so.

MCP and plugin

  • skillhook mcp --cloud, registered by the plugin as skillhook-cloud next to skillhook: the cloud's tools (its names, descriptions, schemas and instructions), each call forwarded with the key and validated, authorised and audited by the cloud; plus generate_secret (sealed end to end). Without a key it offers only skillhook_cloud_setup, which says what is missing and loads the tools once the person logged in (tools/list_changed); the agent never handles the key.
  • New plugin skill skillhook-cloud: start with describe_cloud, triage by failure kind, rejected deliveries and failing checks, act safely (confirm destructive actions, observe mode), set up machines, skills, secrets and hosted URLs.

Link

  • After skill.put, skill.delete, config.patch and secret.* the next sync carries a fresh snapshot, so the dashboard (and list_skills) shows a saved skill and its secret at once instead of up to a minute later. The CLI also falls back to the machine's own answer (get_skill) for a skill saved a moment ago.

Release 0.7.0 (also carries #15, --help never runs a command). Docs: docs/cloud.md ("The whole organisation with an API key"), docs/mcp.md (the cloud server), README, llms.txt, AGENTS.md (layout, outbound requests), plugin manifests, CHANGELOG.

Review fixes (independent review of this PR)

The review found that an agent could use the local update_config tool to point cloud.url at another host and have the bridge send the organisation API key there. Fixed by binding the key to the cloud it was checked against and refusing cloud.* / SKILLHOOK_CLOUD_* changes from the local MCP. Also fixed: argument order around switches, --body needing a special case, control characters in keys and the overview, misleading "unknown subcommand" errors, one bad catalogue entry failing the whole catalogue (now skipped; a newer catalogue shape asks for an update), a catalogue tool being able to replace the local sealed generate_secret, and secret names beyond a skill's own.

Second review (of the fixes): flags before a tool's name were dropped (now read); skillhook's global parser and a tool's parser read --json/--help/-v differently (now reserved words for both); the docs overstated what keeps an agent from moving the key, and a skill naming SKILLHOOK_CLOUD_API_URL (or the admin token) as its secret_env could have it overwritten, also by the cloud through secret.set (now refused by the skill schema and by the resolved name on the machine); an environment key ignored the cloud kept with it and login forgot the previous cloud; a malformed catalogue schema could take the MCP bridge down (now skipped, and registration failures are contained); printable missed C1 controls, bidi overrides and error output.

Third review: a tool's switch before its name could swallow the name, and words before cloud never reached the tool (now: only skillhook's own options before the subcommand or tool, a usage error otherwise); --no-json/--no-help/--no-dir are skillhook's options too; login without --url could check a new key against a stale cloud (now it asks when the kept cloud and the machine's differ); --json output escapes C1 and bidi characters; cloud secret refuses the machine's own credentials even through a skill an older machine still has. Also: with cloud.upload_payloads false (or an organisation that keeps no bodies) the machine no longer sends a job's payload, event or prompt artifact.

Final review: -- is never a parameter's value and works again before the subcommand (skillhook cloud -- status); a blank --url is refused instead of skipping the two-clouds check, which now comes before the key is asked for; the withheld-body reason names both causes. The docs now say exactly what cloud.upload_payloads covers (a delivery's body and a job's payload, event and prompt): an agent's transcript and result are its own output and can quote what it read, and cloud.upload_artifacts: false keeps transcripts home.

Verification

  • npm run check: typecheck, 303 tests (new src/cloud/tools.test.ts, src/cloud/bridge.test.ts, a CLI end-to-end test against the fake cloud's catalogue, the --help coverage lines for every new subcommand, and a link test that fails without the snapshot fix), build, schema, release metadata.
  • End to end against the real Skillhook Cloud running locally with this build paired as a control-mode machine: login, overview, tools, run_skill, get_job_artifact, an agent's question answered with answer_job (delivered live), save_skill then cloud secret (decrypted only on the client), enable_hosted_url with a real webhook through it (accepted, run, via ingress), stats, audit log, and skillhook mcp --cloud over stdio (42 tools for an admin key; only skillhook_cloud_setup without a key). After the second review: cloud --machine e2e-mac list_jobs, --answer --json refused with the --answer=--json hint (nothing sent), -v printing the version, ESC and U+202E stripped from a cloud error, and cloud status for a bound and an unbound key. After the third: options before cloud or before a tool's name refused, login asking which cloud when two differ, --json carrying \u202e as an escape.

🤖 Generated with Claude Code

JOsacky and others added 7 commits October 2, 2026 13:35
…he skillhook-cloud MCP server (0.7.0)

Everything the Skillhook Cloud dashboard shows and does, with an organisation API key. The cloud
publishes its tools (GET /api/v1/tools: JSON Schema and scope for each) and skillhook builds its
commands and MCP tools from that list at run time, so a tool the cloud adds needs no new skillhook.

- skillhook cloud overview: what needs a person across the organisation (describe_cloud).
- skillhook cloud tools [tool] and skillhook cloud <tool> [args] [--param value]: any tool by name,
  required parameters as arguments, flags typed from the schema (switches, checked numbers, JSON as a
  literal, @file or -, --param-file for long text, --input for the whole input), the answer as text
  or as it came with --json; a tool beyond the key's scope is refused before anything is sent.
- skillhook cloud secret <machine> <skill|NAME>: the machine generates the secret sealed to a one-time
  key pair here (POST /machines/<m>/secrets, POST /commands/<id>/claim); the cloud never sees it.
- skillhook mcp --cloud, registered by the plugin as skillhook-cloud: the same tools for the agent,
  the cloud's instructions, generate_secret; without a key only skillhook_cloud_setup, which loads
  the tools once the person logged in (the agent never handles the key).
- cloud login --url names the cloud (kept as cloud.url, refused over a pairing elsewhere) and asks
  for the key without echo at a terminal; cloud status / cloud_status say whether a key is kept.
- The link sends a fresh snapshot right after skill.put, skill.delete, config.patch and secret
  commands, so the dashboard shows a saved skill and its secret at once, not at the next snapshot.
- A plugin skill, skillhook-cloud: triage (failure kinds, rejected deliveries, failing checks),
  acting safely, and setting up machines, skills, secrets and hosted URLs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rts: an MCP client waits for the server meanwhile

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…int it, tools read their own arguments

- cloud login keeps the key with the cloud it was checked against (SKILLHOOK_CLOUD_API_URL in .env)
  and every API request goes there, whatever cloud.url says later; login no longer touches
  cloud.url, so it can never move a machine's link (also one paused with cloud.enabled: false).
- The local MCP server refuses cloud.* settings in update_config and SKILLHOOK_CLOUD_* names in
  set_secret and generate_secret: an agent could otherwise point cloud.url (or the key's URL)
  at another host and have the bridge send the organisation's key there.
- skillhook cloud <tool> reads the arguments after the tool's name with its schema: a switch never
  swallows the next word (save_skill m --allow-unauthenticated s kept m, s in order), a text
  parameter always takes it (--body works without a special case), --x=value, --no-x and -- work,
  and --input yields to flags.
- cloud secret and generate_secret generate only a skill's secret: the skill, SKILLHOOK_SECRET_*,
  or a variable a skill there names; never the machine's admin token or a runner's key. A claim
  that finds the value already collected or expired says so.
- Text from the cloud is printed without control characters (keys too, and the overview, tables
  and job views); --json is unchanged.
- The catalogue keeps the entries it can read and the first of a duplicated name, refuses a
  newer catalogue shape with "update skillhook", and never lets a catalogue tool replace the local
  generate_secret.
- Only a missing key turns an unknown word into "unknown subcommand"; SKILLHOOK_NO_CLOUD or a bad
  URL is the command's failure (exit 1, JSON with --json).
- Docs: login and secrets in docs/cloud.md, the bridge's catalogue lifetime in docs/mcp.md,
  pairing on the dashboard in the plugin skill, CHANGELOG, AGENTS.md, llms.txt.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ill cannot name skillhook's credentials

- skillhook cloud <tool>: the tool's options may come before its name too (they were dropped). skillhook's own
  options (--json, --help/-h, --version/-v, --dir/--home) mean the same wherever they stand and are never a
  parameter's value, so both readings of the line agree and --help never runs a tool; such a text is
  --answer=--help.
- A skill's secret_env can no longer be SKILLHOOK_ADMIN_TOKEN or SKILLHOOK_CLOUD_*: generating or setting the
  skill's secret would overwrite it. The machine refuses the cloud's secret.generate and secret.set for those by
  the resolved name (a skill's, "admin", the variable's), so the cloud cannot rotate the admin token either.
- The API key's cloud: the same key from the environment goes to the cloud login kept with it; login defaults to
  the cloud logged in to before; a 0.6 key without one goes to the machine's cloud and cloud status says so; the
  unreachable hint no longer points at cloud.url.
- A catalogue entry whose input schema is not an object of object parameters is skipped, and a tool the MCP
  server refuses is left out, never taking the bridge down.
- printable() also drops C1 controls and bidirectional overrides and isolates, and covers error output and every
  cloud command's text.
- Docs no longer claim an agent cannot move the key: the local MCP's configuration and secret tools refuse it, an
  agent that runs commands acts with the person's rights.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, in job artifacts too

job.artifact and job.get sent a job's payload, event and prompt (prompt.md quotes the payload) whenever artifacts
could be uploaded, whatever cloud.upload_payloads or the organisation's hint said: the cloud discarded them, or a
machine that kept its bodies home in an organisation that stores them had them stored anyway. They are now refused
(job.artifact) or listed as withheld (job.get) when bodies may not leave the machine.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…'s too, login never guesses between two clouds

- skillhook cloud: before the subcommand or tool only skillhook's own options. A tool's switch that only its schema
  knows (--store-payloads, --include-body) swallowed the tool's name (cloud --store-payloads update_settings ran
  cloud status), and words before `cloud` never reached the tool: both are now usage errors that say where options go.
- --no-json, --no-help, --no-dir… are skillhook's own options as every command reads them: never a tool's value.
- login without --url, when the cloud kept with the last key and the machine's cloud differ, asks for --url instead of
  sending the new key to one of them; a refused key's hint names the cloud (login --url <it>).
- --json output escapes DEL, C1 and bidirectional characters (\u…): the same JSON, nothing a terminal acts on.
- cloud secret (and generate_secret) refuse the machine's own credentials by name or through a skill an older machine
  still has naming one; docs/security.md no longer says the local MCP refuses the admin token.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…blank --url is refused, the docs say what bodies mean

- `--` ends the options for every command, so a tool parameter never takes it as its value (`--answer -- --help`
  answered "--"); before the subcommand it works again (`skillhook cloud -- status`), every word after it an argument.
- login: a blank --url is refused instead of skipping the two-clouds check; that check comes before the key is asked
  for at a terminal.
- The reason for a withheld body names both causes (cloud.upload_payloads here, or an organisation that keeps none);
  the hint path is tested for artifacts.
- docs: cloud.upload_payloads covers a delivery's body and a job's payload, event and prompt; an agent's transcript and
  result are its own output and can quote what it read (cloud.upload_artifacts keeps transcripts home). The protocol
  doc gives the artifact refusal; docs/security.md says API-key requests go to the key's cloud, not cloud.url.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@JOsacky
JOsacky merged commit c6143af into main Oct 2, 2026
6 checks passed
@JOsacky
JOsacky deleted the claude/cloud-toolkit branch October 2, 2026 19:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant