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
Read the source on GitHubView the package on npm
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 0One 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.
{ok:true, data, meta:{count, truncated}}
{ok:false, error, exit}
| exit | meaning |
|---|---|
| exit 0 | ok |
| exit 1 | error (bad flag, bad field, bad request) |
| exit 2 | not found (adapter, verb, window or element) |
| exit 3 | blocked (a DashClaw guard you chose to set up said no, desktop not armed, or a STOP file; never without a guard key) |
| exit 4 | auth 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 0Less 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
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}
$ 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"}}}
$ declick describe nosuchthing
{"ok":false,"error":"no adapter named nosuchthing; run: declick list","exit":2}
$ declick list --fields nope
{"ok":false,"error":"no field matched nope; available: name, engine, source, verbs, lastRun, lastError, auth","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"}}}
meta.governance.exit 1The 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
$ declick lint petstore
{"ok":true,"data":{"name":"petstore","errors":[],"verbs":19},"meta":{"count":1,"truncated":false}}
$ 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}
$ 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
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}}
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.
| engine | source | what is specific to it |
|---|---|---|
| start here: a spec, a server, a database, a chain | ||
openapi | spec.json | spec.yaml | https://.../openapi.json | openapi 3 and swagger 2, json or yaml; a url spec is fetched once at compile time |
mcp | mcp:<command args> | mcp:https://host/mcp | stdio servers spawn the command; http servers take a bearer from <NAME>_TOKEN |
sqlite | sqlite:<path> | data.db | introspects tables and views into list, get, insert, update, delete and a parameterized query |
compose | compose:<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 | ||
graphql | graphql:https://.../graphql | schema.json | schema.graphql | introspects the endpoint; bearer from <NAME>_TOKEN when it answers 401 |
postman | collection.json | insomnia.json | postman v2.1 collections and insomnia v4 exports; recorded secrets become env keys |
har | capture.har | browser network capture; --host picks the API host when the capture has several |
cli | cli:<binary> [fixed args] | compiled from the tool own --help; the binary must be on PATH |
web | web:https://<site> --recipes <dir> | needs Chrome or Edge; one recipe json per verb, and errors carry candidates instead of screenshots |
desktop | app:<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}}
$ declick engines --source app:Calculator
engine=desktop ready=true why=app:Calculator routes to the desktop engine next=declick add app:Calculator --name <name>
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 today | what declick does with it | what the agent reads |
|---|---|---|
| MCP server | declick add "mcp:node fixtures/mcp-server.mjs" --name demo | describe demo, 6 lines |
| Screenshot loop | declick web tree <url> | elements, 330 bytes for example.com |
| Hand-written wrapper | declick add <spec> | manifest, launcher, SKILL.md, lint-checked |
Read the MCP vs CLI measurementHow the SKILL.md is generatedAll guides
$ 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
$ 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"}}
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/binon 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
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
$ 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}}
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}}
$ declick audit --limit 1
2026-09-03T03:59:50.435Z calculator add exit 3 skipped 954ms
{"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":[...]}}
| the authoring session may | the authoring session may not |
|---|---|
|
|
$ 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"}}}
$ 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"}]
--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}
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"}}}
$ 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)"}}}
| action | risk score |
|---|---|
| DELETE | 70 |
| PUT and PATCH | 55 |
| POST | 45 |
| desktop | 60 |
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
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);
}
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]"
}
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;
--revertundoes 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
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.
| needs | what needs it | checked by |
|---|---|---|
| Node 24 or newer | everything | declick version |
| Windows plus deskclaw 0.3.0 or newer | desktop engine | declick doctor |
| Chrome or Edge | web engine | declick doctor |
| Target binary on PATH | cli engine | declick doctor |
| Claude Code CLI on PATH | author, repair, add --goal | declick doctor |
<NAME>_TOKEN | private mcp and graphql servers | declick auth <name> |
| DashClaw key and URL | the guard, optional | declick doctor |
| not in 0.6.2 | status |
|---|---|
| macOS or Linux desktop backend | not shipped |
$ declick doctor --fields node,blocking
{"ok":true,"data":{"node":{"version":"24.15.0","ok":true,"need":">=24"},"blocking":[]},"meta":{"count":1,"truncated":false}}
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]
$ 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)