A compiler for the interface your agent already has: the shell.

Your agent has a shell. MCP makes it carry every tool's schema on every turn. declick compiles an API, an MCP server, or a database once into named verbs the model loads one at a time, and every verb answers in one envelope with five exit codes. Ten engines, zero runtime dependencies, Node 24.

Install declick and compile a specRequest a license

npm i -g declick

declick reads an OpenAPI or Swagger spec, a GraphQL API, an MCP server, a Postman collection, a HAR capture, a SQLite file, another CLI's own --help, a browser page, or a Windows app once, and writes a command line tool with named verbs. Every verb returns the same envelope: success is {ok:true, data, meta}, failure is {ok:false, error, exit}. The same five exit codes on every adapter, and on declick itself. The block on the right is the whole surface of the live Swagger Petstore spec as an agent sees it: 19 verbs in 1,583 characters, under the 2,000-character ceiling declick lint enforces.

$ declick describe petstore
petstore (openapi)  source: https://petstore3.swagger.io/api/v3/openapi.json  base: https://petstore3.swagger.io/api/v3
run: petstore <verb> [args] [--flags]   or: declick run petstore <verb> ...
  update-pet                    Update an existing pet. [mutating]
  add-pet                       Add a new pet to the store. [mutating]
  find-pets-by-status           Finds Pets by status.
  find-pets-by-tags             Finds Pets by tags.
  get-pet-by-id <petId>         Find pet by ID.
  update-pet-with-form <petId>  Updates a pet in the store with form data. [mutating]
  delete-pet <petId>            Deletes a pet. [mutating]
  upload-file <petId>           Uploads an image. [mutating]
  get-inventory                 Returns pet inventories by status.
  place-order                   Place an order for a pet. [mutating]
  get-order-by-id <orderId>     Find purchase order by ID.
  delete-order <orderId>        Delete purchase order by identifier. [mutating]
  create-user                   Create user. [mutating]
  create-users-with-list-input  Creates list of users with given input array. [mutating]
  login-user                    Logs user into the system.
  logout-user                   Logs out current logged in user session.
  get-user-by-name <username>   Get user by user name.
  update-user <username>        Update user resource. [mutating]
  delete-user <username>        Delete user resource. [mutating]
common: --json --fields --limit --where --dry-run --full --help   exit: 0 ok 1 err 2 missing 3 blocked 4 auth
auth env: PETSTORE_PETSTORE_AUTH, PETSTORE_API_KEY
declick describe petstore, 1,583 characters, 19 verbsexit 0
{"ok":true,"data":{"version":"0.6.2","node":"24.15.0"},"meta":{"count":1,"truncated":false}}
declick version --jsonexit 0

One envelope, and it does not vary by engine

--json is the default when stdout is not a TTY, so an agent gets structured output without asking. Success carries data plus meta.count and meta.truncated. Failure carries the error string and the exit code, and data too when the engine has a payload, such as an API error body or a desktop tree diff. --fields projects named fields and takes dotted paths like error.code or items.0.name. --limit caps a list at 50 by default. --rows unwraps a nested array and records the path in meta.rows while meta.extra keeps the siblings. --where k=v narrows a list before --fields and --limit run, --max-bytes N caps what data can carry, and --cache <s> answers a read-only verb from a stored response instead of calling the engine again; the section below has the detail. --each file runs a verb once per item in a file or stdin and answers with one envelope, meta.count and meta.failed covering the whole batch. ~/.declick/<name>/defaults.json supplies flags you would otherwise retype on every call, and a run that used one lists the keys in meta.defaults. Both --flag value and --flag=value parse, boolean flags never eat the next argument, and an unknown flag is exit 1, never ignored. The shape is identical across all ten engines and across declick's own commands.

success
{ok:true, data, meta:{count, truncated}}
failure
{ok:false, error, exit}
exitmeaning
exit 0ok
exit 1error (bad flag, bad field, bad request)
exit 2not found (adapter, verb, window or element)
exit 3blocked (a DashClaw guard you chose to set up said no, desktop not armed, or a STOP file; never without a guard key)
exit 4auth needed (declick auth <name> says which key)
$ declick run demo list-notes
{"ok":true,"data":[{"id":"n1","title":"A"},{"id":"n2","title":"B"}],"meta":{"count":2,"truncated":false,"rows":"notes","extra":{"total":2,"echo":{}},"governance":{"enabled":false,"decision":"skipped","reason":"read-only verb"}}}
meta.rows names what was unwrapped, meta.extra keeps the siblings.exit 0

Less lands in the context, and it is measured

--where k=v filters a list on the machine that has it, before --fields and --limit run. It is repeatable and comma-separable, takes dotted paths, and every condition has to hold: eight operators, k=v, k!=v, k~re (case-insensitive regex), k>n, k>=n, k<n, k<=n, and k=* for present and not null. meta.where reports {matched, of}, and meta.count is what matched.

data also carries a ceiling: 8192 bytes by default, moved for every call with DECLICK_MAX_BYTES or for one with --max-bytes N, and turned off with 0. Over the cap a list drops tail rows and keeps at least one, a string is sliced, and an object keeps every key with its biggest values replaced by <N bytes; add --fields or --limit>, so the shape needed to write a --fields survives the cut. meta.capped names the size with a hint, meta.truncated is true, and the exit code stays 0; the cap never touches --dry-run, --help or describe.

--cache <seconds> on a read-only verb answers from the response the wire already gave. The key is the adapter, the verb, its positional args and its own flags, not --fields or --limit, so the same call shaped four ways reads one stored entry. And now that every audit line carries bytes, declick audit --sum adds the run log up instead of listing it, one row per adapter and a total: 212 calls, 340.2 KB read through adapters, 3 failed.

declick daemon start keeps stdio MCP servers warm in a detached per-user process, so only the first declick run <mcp adapter> <verb> pays a server's startup. A run tries the daemon with a 300ms connect budget and falls back to spawning its own server when nothing answers, so nothing breaks when the daemon is not up; the endpoint is a per-user named pipe on Windows or a 0600 socket elsewhere, plus a token, and a served run carries meta.daemon: true.

$ node scripts/bench-tokens.mjs
adapter                             tools  raw init+tools/list   declick describe   ratio
nine stdio MCP servers, combined      258        236,818 bytes         58,309 bytes    4.1x
Measured against nine real stdio MCP servers, full breakdown in docs/bench.md. A single call's payload is not smaller through declick, 0.8x on the fs server; the saving is paying once for the surface listing instead of every tool description riding in the prompt.

A miss names what exists

An unknown flag is exit 1 with the nearest match. An unknown verb is exit 2 with the nearest verb names, so a guess costs one turn, not two. A missing adapter is exit 2 with the command that lists them. A --fields list that matches nothing is exit 1 naming the keys that were available; a partial miss lands in meta.unknownFields and the call still succeeds. Every error string ends with a runnable next command, so an agent retries with a real argument instead of guessing twice. Adapter-run failures also carry meta.governance, so the audit trail is complete even on a typo.

$ declick version --nope
{"ok":false,"error":"unknown flag --nope for version; did you mean --rows?; run: declick version --help","exit":1}
An unknown flag names the nearest match.exit 1
$ declick run petstore get-pet 7
{"ok":false,"error":"unknown verb get-pet; run: declick describe petstore; did you mean get-pet-by-id, add-pet?","exit":2,"meta":{"governance":{"enabled":true,"decision":"skipped","reason":"no mutating action"}}}
An unknown verb names the nearest verbs.exit 2
$ declick describe nosuchthing
{"ok":false,"error":"no adapter named nosuchthing; run: declick list","exit":2}
A missing adapter names the command that lists them.exit 2
$ declick list --fields nope
{"ok":false,"error":"no field matched nope; available: name, engine, source, verbs, lastRun, lastError, auth","exit":1}
A field miss names the keys that were available.exit 1
$ declick run petstore get-pet-by-id 7 --pretty
{"ok":false,"error":"unknown flag --pretty for get-pet-by-id; run: declick describe petstore --verb get-pet-by-id; did you mean --retry?","exit":1,"meta":{"governance":{"enabled":false,"decision":"skipped","reason":"no mutating action"}}}
An adapter-run failure keeps meta.governance.exit 1

The whole surface, under 500 tokens, or the build fails

declick describe prints every verb, its required args and its flags in one block small enough to hold in context. --full adds per-verb flags and a -> line with the compiled returns shape, read from the first 2xx response schema. --verb isolates one verb, --grep and --offset page a large adapter, and --json emits the same thing as data. The 2,000-character ceiling is enforced by declick lint at build time: compiling all of git produces 2,284 characters of describe, so the build fails and prints every verb you could narrow to. Add --verbs status,log,diff and the same tool builds at a size an agent can read.

$ declick describe petstore --verb add-pet --full
petstore (openapi)  source: https://petstore3.swagger.io/api/v3/openapi.json  base: https://petstore3.swagger.io/api/v3
run: petstore <verb> [args] [--flags]   or: declick run petstore <verb> ...
  add-pet  Add a new pet to the store. [mutating]
      --body  raw application/json body
      --id    e.g. 10
      --name (required)    e.g. doggie
      --category.id    e.g. 1
      --category.name    e.g. Dogs
      --photoUrls (required)
      --tags
      --status  pet status in the store  one of available|pending|sold
      -> {id, name, category, photoUrls, tags, status}
common: --json --fields --limit --dry-run --full --help   exit: 0 ok 1 err 2 missing 3 blocked 4 auth
request: --header --base-url --server --content-type --body-file --output --retry --timeout --curl --verbose
auth env: PETSTORE_PETSTORE_AUTH, PETSTORE_API_KEY
One verb, its flags, and the compiled returns shape.exit 0
$ declick lint petstore
{"ok":true,"data":{"name":"petstore","errors":[],"verbs":19},"meta":{"count":1,"truncated":false}}
The 19-verb adapter passes lint.exit 0
$ declick add cli:git --name gitx
{"ok":false,"error":"lint failed:\n  describe is 2284 chars; limit 2000 (about 500 tokens); narrow with: declick add <source> --verbs a,b or --tag t\n  verbs: clone, init, add, mv, restore, rm, bisect, diff, grep, log, show, status, backfill, branch, commit, merge, rebase, reset, switch, tag, fetch, pull, push","exit":1}
The tool refuses its own oversized output.exit 1
$ declick add cli:git --name gitx --verbs status,log,diff
$ declick describe gitx
gitx (cli)  source: cli:"C:\Program Files\Git\mingw64\bin\git.exe"  bin: C:\Program Files\Git\mingw64\bin\git.exe diff
run: gitx <verb> [args] [--flags]   or: declick run gitx <verb> ...
  diff [commit] [path]         Show changes between commits, commit and working tree, etc
  log [revision-range] [path]  Show commit logs
  status [pathspec]            Show the working tree status
common: --json --fields --limit --full --help   exit: 0 ok 1 err 2 missing 3 blocked 4 auth
The same tool, narrowed to three verbs.exit 0

Every write previews before it happens

Every mutating verb takes --dry-run. It prints the exact request it would send, sets meta.dryRun true, and sends nothing. The management commands that write take it too: add, build, accept, import, skill, remove, path --install, and desk arm or disarm. author, repair and ui refuse the flag, because a bounded Claude session has no preview to give. Secrets appear as their env key names in the preview, never as values. The governance object in a dry run reads enabled false with no key set, which is the state of a fresh install.

$ declick run petstore add-pet --name Rex --dry-run
{"ok":true,"data":{"method":"POST","url":"https://petstore3.swagger.io/api/v3/pet","headers":{"accept":"application/json","content-type":"application/json","authorization":"Bearer <PETSTORE_PETSTORE_AUTH>"},"body":"{\"name\":\"Rex\"}"},"meta":{"count":1,"truncated":false,"governance":{"enabled":false,"decision":"dry-run","reason":"preview only, nothing sent"},"dryRun":true}}
Captured with no guard key set. The header carries the env key name, never a value.exit 0

takes --dry-run

Every mutating verb, plus these management commands. add is the one exception: with --goal and no --recipes it is authoring, which has no preview.

  • add
  • build
  • accept
  • import
  • skill
  • remove
  • path --install
  • desk arm
  • desk disarm

refuses --dry-run

  • author
  • repair
  • ui

Start with four engines. Six more are compiled in.

Most agents need four: openapi for a spec, mcp for a server the agent already has, sqlite for a database, and compose to name a chain of verbs the model would otherwise rediscover every session. Nothing outside declick runs for any of them. The six below cover a source the first four do not read: a GraphQL endpoint, a Postman or Insomnia export, a HAR capture, another tool's --help, a browser page, a Windows window. The last three drive something live, so they carry the flakiness of that thing; the first seven do not.

Every engine is compiled into declick, so there is no plugin to find and no service in the path. Four of them shell out to something you already have: web needs Chrome or Edge, desktop needs deskclaw, cli needs the target binary on PATH, and authoring needs the Claude Code CLI. compose shells out too, but only to declick itself: every step in a chain runs as its own guarded, audited command, not a third-party binary. The requirements section lists all of it. declick engines --source says which engine a source lands on, and why, before anything is written. A URL spec is fetched once at compile time and never again at run time.

enginesourcewhat is specific to it
start here: a spec, a server, a database, a chain
openapispec.json | spec.yaml | https://.../openapi.jsonopenapi 3 and swagger 2, json or yaml; a url spec is fetched once at compile time
mcpmcp:<command args> | mcp:https://host/mcpstdio servers spawn the command; http servers take a bearer from <NAME>_TOKEN
sqlitesqlite:<path> | data.dbintrospects tables and views into list, get, insert, update, delete and a parameterized query
composecompose:<chain.json>chains verbs from adapters already built into one verb; every step still runs as its own guarded, audited command
also compiled in: other spec formats, and sources with no spec
graphqlgraphql:https://.../graphql | schema.json | schema.graphqlintrospects the endpoint; bearer from <NAME>_TOKEN when it answers 401
postmancollection.json | insomnia.jsonpostman v2.1 collections and insomnia v4 exports; recorded secrets become env keys
harcapture.harbrowser network capture; --host picks the API host when the capture has several
clicli:<binary> [fixed args]compiled from the tool own --help; the binary must be on PATH
webweb:https://<site> --recipes <dir>needs Chrome or Edge; one recipe json per verb, and errors carry candidates instead of screenshots
desktopapp:<window title>needs deskclaw; declick doctor checks it
$ declick engines --json
{"ok":true,"data":[{"name":"openapi","ready":true,"source":"spec.json | spec.yaml | https://.../openapi.json","note":"openapi 3 and swagger 2, json or yaml; a url spec is fetched once at compile time"},{"name":"desktop","ready":true,"source":"app:<window title>","note":"needs deskclaw; declick doctor checks it"},{"name":"mcp","ready":true,"source":"mcp:<command args> | mcp:https://host/mcp","note":"stdio servers spawn the command; http servers take a bearer from <NAME>_TOKEN"},{"name":"web","ready":true,"source":"web:https://<site> --recipes <dir>","note":"needs Chrome or Edge; one recipe json per verb, and errors carry candidates instead of screenshots"},{"name":"graphql","ready":true,"source":"graphql:https://.../graphql | schema.json | schema.graphql","note":"introspects the endpoint; bearer from <NAME>_TOKEN when it answers 401"},{"name":"postman","ready":true,"source":"collection.json | insomnia.json","note":"postman v2.1 collections and insomnia v4 exports; recorded secrets become env keys"},{"name":"har","ready":true,"source":"capture.har","note":"browser network capture; --host picks the API host when the capture has several"},{"name":"sqlite","ready":true,"source":"sqlite:<path> | data.db","note":"introspects tables and views into list, get, insert, update, delete and a parameterized query"},{"name":"cli","ready":true,"source":"cli:<binary> [fixed args]","note":"compiled from the tool own --help; the binary must be on PATH"},{"name":"compose","ready":true,"source":"compose:<chain.json>","note":"chains verbs from adapters already built into one verb; every step still runs as its own guarded, audited command"}],"meta":{"count":10,"truncated":false}}
The payload the table above was built from.exit 0
$ declick engines --source app:Calculator
engine=desktop  ready=true  why=app:Calculator routes to the desktop engine  next=declick add app:Calculator --name <name>
The classifier explaining its own routing.exit 0

If your agent already has MCP servers or a screenshot loop

An MCP server's tool list rides in every prompt. Compile it with declick and the agent reads describe once, under 500 tokens, then calls verbs from a shell it already has; a nested payload comes back as rows with meta.rows naming what was unwrapped. A screenshot agent puts an image in the context window for every step. declick web tree returns the page as elements, interactive ones first, and the desktop engine replays element paths against a fresh window tree. declick web text <url> --grep re returns the page's own text filtered by a regex, for the times an agent needs the words on the page rather than its elements. The example.com tree below is 330 bytes and contains no image. A hand-written wrapper is a parser you maintain per tool. declick writes the parser, the launcher, and the SKILL.md, and lint fails the build when the surface grows past what an agent can hold.

what you run todaywhat declick does with itwhat the agent reads
MCP serverdeclick add "mcp:node fixtures/mcp-server.mjs" --name demodescribe demo, 6 lines
Screenshot loopdeclick web tree <url>elements, 330 bytes for example.com
Hand-written wrapperdeclick add <spec>manifest, launcher, SKILL.md, lint-checked
$ declick describe demo
demo (mcp)  source: mcp:node fixtures/mcp-server.mjs  server: node fixtures/mcp-server.mjs
run: demo <verb> [args] [--flags]   or: declick run demo <verb> ...
  list-notes        List notes, newest first.
  add-note <title>  Add a note to the book [mutating]
  boom              Always reports a tool error [mutating]
common: --json --fields --limit --rows --dry-run --full --help   exit: 0 ok 1 err 2 missing 3 blocked 4 auth
The whole MCP server, read once.exit 0
$ declick web tree https://example.com --limit 6
{"ok":true,"data":[{"role":"link","name":"Learn more","id":null,"href":"https://iana.org/domains/example","value":null,"interactive":true},{"role":"heading","name":"Example Domain","id":null,"href":null,"value":null,"interactive":false}],"meta":{"count":2,"truncated":false,"url":"https://example.com/","title":"Example Domain"}}
330 bytes, no image, no vision tokens.exit 0

One command wires your agent. One command unwires it.

Integrating declick into an agent by hand means putting the bin on PATH, compiling the agent's existing MCP servers into adapters, adding a rules block to its instruction file, and, for Claude Code, registering a hook that reminds the model an adapter exists. declick setup does all four, and declick setup --dry-run prints the plan first and writes nothing.

Before any of it writes a file, setup takes a byte-exact snapshot under ~/.declick/setup/<timestamp>/. declick setup --revert reads the latest snapshot: a file untouched since setup ran is restored byte for byte, or deleted if setup created it; a file you edited since keeps your edits and loses only the rules block or the hook entry. The snapshot carries a standalone revert.mjs, so node ~/.declick/setup/<timestamp>/revert.mjs restores files with no dependency on the package, even after npm rm -g declick. declick uninstall --yes runs a revert if one is available, deletes ~/.declick entirely, and prints the npm rm -g declick line.

what setup does

  • 1. PATH: puts ~/.declick/bin on it
  • 2. adopts the agent's MCP servers as adapters
  • 3. adds a rules block to CLAUDE.md or AGENTS.md
  • 4. installs the Claude Code PreToolUse hook

what revert restores

  • unedited file → restored byte for byte
  • edited file → edits kept, only the block or hook entry removed
  • file setup created → deleted
$ declick setup --dry-run
$ declick setup
$ declick setup --revert
$ declick uninstall --yes
Preview, wire it in, undo it, then remove declick's own home directory.exit 0

Windows apps replay as element paths, not pixels

A desktop adapter is a recipe of steps that locate elements by ControlType:Name paths against a fresh snapshot of the window tree, with backtracking, never by screen coordinates. Authoring runs one bounded Claude Code session that may only read the window tree through deskclaw; declick dry-runs the proposal to prove every path resolves, routes a mutating replay through the guard, replays once for real, and saves only when the result matches the expect regex. After that there is no model in the loop. When the app changes, the run exits 2 with a diff of the recorded tree against the live one, and declick repair reruns authoring seeded with that diff. Acting is gated by a timer: declick desk arm 15 opens a window of a few minutes, and an unarmed call or a STOP file is exit 3. The Calculator recipe ships in the repo at fixtures/calculator, so the block below reproduces on any Windows machine with deskclaw installed.

$ declick add app:Calculator --name calculator --recipes fixtures/calculator
$ declick describe calculator
calculator (desktop)  source: app:Calculator  window: "Calculator"
run: calculator <verb> [args] [--flags]   or: declick run calculator <verb> ...
  add <a> <b>  Add two digits and read the display [mutating]
common: --json --fields --limit --dry-run --help   exit: 0 ok 1 err 2 missing 3 blocked 4 auth
The compiled adapter describing itself.exit 0
$ declick run calculator add Seven Seven --dry-run
{"ok":true,"data":{"steps":[{"window":"Calculator"},{"would":"wait-for","find":["Text:Display is *"],"timeout":5000},{"found":["Group:Number pad","Button:Seven"],"ref":"@e64"},{"would":"click","ref":"@e64","name":"Seven"},{"found":["Group:Standard operators","Button:Plus"],"ref":"@e53"},{"would":"click","ref":"@e53","name":"Plus"},{"found":["Group:Number pad","Button:Seven"],"ref":"@e64"},{"would":"click","ref":"@e64","name":"Seven"},{"found":["Group:Standard operators","Button:Equals"],"ref":"@e54"},{"would":"click","ref":"@e54","name":"Equals"},{"found":["Text:Display is *"],"ref":"@e10"},{"read":"result","prop":"name"}]},"meta":{"count":1,"truncated":false,"governance":{"enabled":false,"decision":"dry-run","reason":"preview only, nothing sent"},"dryRun":true}}
Every find returned a live element reference. meta.dryRun is true. Nothing was clicked. The recipe carries a launch block, so Calculator opens on the machine that runs this.exit 0
$ declick desk status
{"ok":true,"data":{"path":"~/.claude/tools/deskclaw/desk","exists":true,"armed":false,"expires":null,"stop":false},"meta":{"count":1,"truncated":false}}
The arm switch as state.exit 0
$ declick audit --limit 1
2026-09-03T03:59:50.435Z	calculator add	exit 3	skipped	954ms
An unarmed real replay, as it appears in the run log.exit 0
{"ok":false,"error":"element not found: Group:Standard operators > Button:Plus Sign in \"Calculator\"; run: declick repair calculator add","exit":2,"data":{"missing":[...],"added":[...],"unresolved":[...]}}
Documented shape from the README, not a capture from this session.exit 2
the authoring session maythe authoring session may not
  • read the window tree through deskclaw
  • propose steps, an example and an expect regex
  • have its recipe replayed once under the governance guard
  • see ANTHROPIC_API_KEY or your other keys (allowlisted environment)
  • run past DECLICK_AUTHOR_TIMEOUT_MS, default 300000
  • save anything whose replay result does not match expect
  • run at all without the Claude Code CLI on PATH

$ declick add app:Calculator --name calculator --goal "multiply two numbers and return the display" --verb multiply

window, launch, find, wait-for, read-all, read, wait-for-text, assert, click, type, key, scroll, expand, collapse, select, context, set, clipboard, dismiss, wait; any step can carry optional: true

Refusals are compiled in. The guard is a tier you turn on.

Tier one, with the guard key unset

Tier one needs no server and no policy file. A key is scoped to the origin the adapter was built against, and a request to another host does not get it: exit 4, with the scoped origin named. Two things release it: DECLICK_ENV_ALLOW naming the key, or an explicit --base-url on the command line, which also prints a warning. Either way the envelope records scopedTo and sentTo in meta.credentials. A live key in --curl or --verbose output is replaced by its env name; only the fetch sees the value. A launcher whose name already resolves on PATH is refused, so compiling Calculator as calc does not shadow calc.exe.

$ PETSTORE_API_KEY=... DECLICK_PETSTORE_BASE_URL=https://api.attacker.example declick run petstore get-pet-by-id 7
{"ok":false,"error":"PETSTORE_API_KEY is scoped to https://petstore3.swagger.io but this request goes to https://api.attacker.example; rebuild the adapter for https://api.attacker.example or set DECLICK_ENV_ALLOW=PETSTORE_API_KEY","exit":4,"meta":{"governance":{"enabled":false,"decision":"skipped","reason":"read-only verb"}}}
The key is scoped to the origin the adapter was built against.exit 4
$ PETSTORE_API_KEY=... declick run petstore get-pet-by-id 7 --curl --fields meta.curl,meta.credentials
"curl":"curl -X GET -H 'accept: application/json' -H 'api_key: <PETSTORE_API_KEY>' 'https://petstore3.swagger.io/api/v3/pet/7'"
"credentials":[{"name":"PETSTORE_API_KEY","from":"env"}]
Projected with --fields from the captured 200 envelope; the two fields are shown verbatim.exit 0
$ declick add app:Calculator --name calc --recipes fixtures/calculator
{"ok":false,"error":"calc already resolves to C:\\Windows\\System32\\calc.exe; pick another --name or pass --force","exit":1}
A launcher name that already resolves on PATH is refused.exit 1

Tier two, opt in

The guard is DashClaw, the approval and policy layer for unattended agents from the same author: it intercepts a risky action before it runs and blocks it, or asks a person to approve it from anywhere. With no DASHCLAW_API_KEY set, a mutating call proceeds with nothing on stderr, and the envelope records governance.enabled false with the reason no guard configured. A local ~/.declick/policy.json decides first, with or without a key: allow, warn or block by adapter and verb, first matching rule wins, no network involved. Set the key and DASHCLAW_URL and every real mutating call posts {action_type, agent_id, agent_name, declared_goal, risk_score, target, systems_touched, tool: {name, engine, method, args}} to your guard first, the key in x-api-key, and the decision lands as an action record on your dashboard, secret-shaped args replaced by <redacted> and everything else truncated at 64 characters, with a 3 second timeout. From then on strict is the default: a guard that is unreachable, times out, or answers anything but a decision is exit 3. DECLICK_GUARD=open restores warn-and-proceed on a guard failure; a block or require_approval the guard actually returns is refused either way. Every invocation appends one line to ~/.declick/audit.jsonl, declick audit reads it newest first, and DECLICK_AUDIT=off turns the log off. That switch is worth knowing about if you are the one who has to prove it was on.

$ declick run demo add-note --title hello --count 1
{"ok":true,"data":{"id":"n3","echo":{"title":"hello","count":1}},"meta":{"count":1,"truncated":false,"governance":{"enabled":false,"decision":"skipped","reason":"no guard configured"}}}
With no guard key set, the call proceeds, nothing is written to stderr, and the envelope says no guard was configured.exit 0
$ DASHCLAW_API_KEY=test-key DASHCLAW_URL=http://127.0.0.1:9 declick run demo add-note --title hello --count 1
{"ok":false,"error":"blocked by governance: governance unreachable (fetch failed) (strict; set DECLICK_GUARD=open to proceed ungoverned)","exit":3,"meta":{"governance":{"enabled":true,"decision":"block","reason":"governance unreachable (fetch failed) (strict; set DECLICK_GUARD=open to proceed ungoverned)"}}}
The same mutating call, with the guard variables on the command line.exit 3
actionrisk score
DELETE70
PUT and PATCH55
POST45
desktop60

allow, warn, block, require_approval, skipped, dry-run, failed-open

require_approval exits 3 and carries data.approvalId.

$ declick audit --limit 3
2026-09-03T03:59:27.835Z	demo list-notes	ok	skipped	42ms
2026-09-03T03:59:27.660Z	demo add-note	exit 3	block	16ms
2026-09-03T03:59:27.509Z	demo add-note	ok	skipped	45ms
The log, newest first.exit 0

Read the control matrix

Any agent with a shell, not only Claude Code

declick is a command line tool, so the integration is the shell. declick add writes the adapter's SKILL.md into every agent skills directory that exists on the machine: ~/.claude/skills for Claude Code, ~/.codex/skills for Codex, ~/.hermes/skills for Hermes, ~/.openclaw/skills for OpenClaw. It never creates a directory for an agent you do not have, and DECLICK_SKILLS names any other list, comma separated. An agent without a skills directory reads declick describe <name>, or you paste declick skill <name> --print into its AGENTS.md or system prompt.

A custom agent on the Anthropic SDK, the OpenAI SDK, or Anthropic Managed Agents needs one tool: run declick run <adapter> <verb> [args] --json and hand back stdout. The envelope is the tool result, the exit code is the status, and describe --json is the source for the tool's input schema. Nothing in that loop is specific to a model or a vendor. Governance is optional in the same way: with no DashClaw key set, mutating verbs run and nothing blocks.

import { execFile } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { promisify } from 'node:util';
const bin = fileURLToPath(import.meta.resolve('declick/bin/declick.mjs'));

// The one tool an SDK agent needs. The envelope is the result, on success and on failure.
export async function declick(adapter, verb, args = []) {
  const r = await promisify(execFile)(process.execPath, [bin, 'run', adapter, verb, ...args, '--json']).catch(e => e);
  return JSON.parse(r.stdout);
}
Source, not output: npm i declick in the agent's project, then this function. Run on Node 24 it returned the row for get-user-by-name user1 and the exit 2 envelope for a bad verb, with no shell involved on any OS.

declick honors the contract it compiles

All 35 commands take --json and return the same envelope. declick commands prints the table as data: every command with its positionals, its flags, whether it mutates, whether it previews, and runnable examples. The shipped declick skill is rendered from that same table, so what an agent reads cannot drift from what the binary does. An agent with only a shell can compile, inspect, run, repair and audit. declick ui serves one page on 127.0.0.1 with the same buttons, for the parts a human should decide.

{
  "name": "add",
  "summary": "compile a source into an adapter: manifest, launcher, SKILL.md",
  "positionals": [{ "name": "source", "required": true }],
  "mutating": true,
  "dryRun": true,
  "examples": [
    "declick add fixtures/petstore.json --name petstore --verbs get-pet-by-id,add-pet",
    "declick add https://api.example.com/openapi.json",
    "declick add app:Calculator --name calc --goal \"add two numbers\" --verb add"
  ],
  "usage": "declick add <source> [--name n] [--verbs a,b] [--tag t] [--engine e] [--goal \"...\"] [--verb v] [--recipes dir|file|-] [--host api.example.com] [--url https://.../graphql] [--force]"
}
One row from declick commands --json, pretty-printed with the values unchanged; the ten-entry flags array is omitted for length.
  • add compile a source into an adapter: manifest, launcher, SKILL.md
  • run invoke a verb without ~/.declick/bin on PATH; the verb keeps its own flags
  • author add a verb to a desktop adapter with Claude (explores, replays once, saves)
  • repair re-author a verb whose element path stopped resolving, seeded with the last failure
  • accept promote a rejected authoring proposal into the adapter
  • build recompile an adapter from its recorded source
  • compose chain verbs from adapters you already have into one verb, or print a chain
  • describe one adapter: verbs, args and returns, under 500 tokens
  • manifest the compiled contract as data (http method/path or recipe steps)
  • lint check an adapter against the output contract
  • list every adapter with its engine, verbs and auth keys
  • status last run, last error with tree diff, proposals and recipes
  • doctor node, home, PATH, deskclaw, claude, vault, engines and their tools
  • daemon start|stop|status: keep stdio MCP servers warm so a run answers in 59ms instead of paying the server's startup every time
  • setup wire declick into the agents on this machine: PATH, adapters, rules, hook; --revert undoes it byte for byte
  • uninstall revert setup if it ran, then delete ~/.declick and print the npm rm -g line
  • auth which env keys a verb needs, and where each one is read from
  • engines the engines this build has, or what one source would compile to
  • path where declick keeps adapters, launchers and skills
  • proposals authoring proposals whose replay failed, waiting to be accepted
  • recipes stored desktop recipes for an adapter
  • recipe one stored recipe, step by step
  • skill regenerate SKILL.md for one adapter or all of them
  • remove delete an adapter (manifest, launcher, skill) or one desktop verb
  • export the adapter and its recipes as one bundle on stdout
  • import install an adapter from a bundle (file, or - for stdin)
  • desk the desktop through deskclaw: windows, element tree, read, clipboard, arm switch
  • web a page as a tree of elements a recipe can click, instead of a screenshot
  • ui local page: every adapter, last run, add / build / repair / remove
  • audit the run log: what ran, what governance decided, what failed
  • policy local rules that allow, warn or block a verb before it runs, with no service
  • defaults flag defaults for an adapter: shown, set, unset or cleared, per verb or for all
  • commands this table as data: every command, its flags and examples
  • version the declick and node versions
  • help the usage table, or one command row

declick commands --json returns "meta":{"count":35,"truncated":false}.

$ declick describe declick
declick 0.6.2: turn anything into a CLI so your agents stop clicking
The first line of the text form.exit 0

What it needs, and what 0.6.2 does not do

Node 24 or newer; declick doctor checks it and keeps blocking problems separate from warnings. The desktop engine is Windows only and needs deskclaw 0.3.0 or newer on PATH; there is no macOS or Linux desktop backend. The web engine needs Chrome or Edge installed. The cli engine needs the target binary on PATH. Authoring or repair needs the Claude Code CLI on PATH for one bounded sonnet session, default timeout 300,000 ms. mcp and graphql against a private server read a bearer from <NAME>_TOKEN. declick ui listens on 127.0.0.1 only. Not shipped in 0.6.2: a macOS or Linux desktop backend.

needswhat needs itchecked by
Node 24 or newereverythingdeclick version
Windows plus deskclaw 0.3.0 or newerdesktop enginedeclick doctor
Chrome or Edgeweb enginedeclick doctor
Target binary on PATHcli enginedeclick doctor
Claude Code CLI on PATHauthor, repair, add --goaldeclick doctor
<NAME>_TOKENprivate mcp and graphql serversdeclick auth <name>
DashClaw key and URLthe guard, optionaldeclick doctor
not in 0.6.2status
macOS or Linux desktop backendnot shipped
$ declick doctor --fields node,blocking
{"ok":true,"data":{"node":{"version":"24.15.0","ok":true,"need":">=24"},"blocking":[]},"meta":{"count":1,"truncated":false}}
Node checked, nothing blocking.exit 0

Free for individuals and small teams. Larger teams buy seats.

The 0.3.0 release on npm is MIT and stays that way. Releases after it are source-available under the Elastic License 2.0: read it, run it, change it, ship it inside your own product. Do not offer it to others as a managed service.

Free. Individuals, and companies with fewer than ten people. Everything included, no key, no sign-up.

Team, $19 per developer per month, or $190 per year. Commercial use at a company of ten people or more. Self-serve, no call.

Production support, $2,000 per year per company. A named contact, a two business day response, a private issue tracker. This is the tier a platform team buys once declick is in a pipeline.

Buy seats, monthlyBuy seats, yearlyBuy production support

Checkout is Stripe. Pick the seat count on the checkout page; the license arrives by email within a minute of paying, names the seats or the support tier it covers, and is your proof of purchase. Receipts and invoices come from Stripe. Anything bigger, or an invoice instead of a card: email for terms, not a sales call.

Four commands to a working adapter

Install it, wire it into your agent, point it at a spec, describe what came out. declick setup is optional: skip it and declick add still works with no configuration. declick add writes three things: the compiled manifest at ~/.declick/<name>/manifest.json, a two line launcher in ~/.declick/bin, and a SKILL.md into every agent skills directory on the machine (Claude Code, Codex, Hermes, OpenClaw) so the agent finds the adapter without being told. It writes declick's own skill too, so an agent that finds one adapter knows how to build the next. declick path --install puts the launcher directory on PATH. Every block on this page is output from declick 0.6.2 on Node 24.15.0; home directory paths are shortened and nothing else is edited. Source on GitHub.

$ npm i -g declick
$ declick setup
$ declick add https://petstore3.swagger.io/api/v3/openapi.json --name petstore
$ declick describe petstore
petstore (openapi)  source: https://petstore3.swagger.io/api/v3/openapi.json  base: https://petstore3.swagger.io/api/v3
run: petstore <verb> [args] [--flags]   or: declick run petstore <verb> ...
  update-pet                    Update an existing pet. [mutating]
  add-pet                       Add a new pet to the store. [mutating]
The first four lines of the describe output; the full 19-verb block is at the top of this page.exit 0
$ petstore get-user-by-name user1 --fields username,email
{"ok":true,"data":{"username":"user1","email":"email1@test.com"},"meta":{"count":1,"truncated":false,"status":200,"governance":{"enabled":false,"decision":"skipped","reason":"read-only verb"}}}
$ petstore get-pet-by-id 7
error: set PETSTORE_API_KEY or PETSTORE_PETSTORE_AUTH in the environment or ~/.creds/vault.env (or run: creds mint petstore)
A real call against the live petstore, then a verb whose spec declares an API key: exit 4 names the key instead of sending the request without it.exit 0exit 4

Install declick and compile a spec