The permission layer — and the paper trail — for AI agents.
English · 简体中文
Before your agent acts, it checks a one-page rule file you wrote: allow, ask, or deny. After, it leaves a tamper-evident record. Anything you didn't write down, the agent must ask about. Plain Markdown in your repo; works with Claude, ChatGPT/Codex, Gemini, Cursor, OpenClaw, Hermes, and any MCP client.
Hand your agent real work. Keep the authority. Get the receipts.
Install: npm install -g docket-agent · Docs:
shahcolate.github.io/docket/docs.html
Zero dependencies · plain Markdown + JSONL · MIT
- 2026.08 —
v0.7.0: policy travels.docket policy push/pullpublishes loops as an OCI artifact, so one baseline reaches forty repos through the registry you already run — with digest verification, a publisher-can't-choose-your-paths rule, and a human keystroke before anything installs.extends:still cannot name a registry, on purpose: a warrant check must never depend on the network. The image now ships to ghcr.io/shahcolate/docket-agent on every release tag, multi-arch, with SBOM and provenance attestations. - 2026.08 —
v0.6.0: the warrant leaves the harness. Enforcement was one vendor's hook; now it's alsodocket intercept, a Docker MCP Gateway interceptor that gates everytools/callfrom any client to any server (20 hostile tool calls in the new red-team suite, zero allowed — and at a gateway, silence is an allow, so crashes count as fail-open). Plusextends:so one baseline governs forty loops and a child can tighten but never loosen, signed record heads that catch a cut tail even after the log grows back, and an official image so the gate runs where there's no Node. (composing with sandboxes) - 2026.07 —
v0.5.0: the record learns who. Every entry is now stamped with the agent, branch, and worktree that wrote it (by), so parallel agents stay separable at merge time — withdocket record log --byanddocket metrics --byto read one agent's posture back. Appends are now serialized across processes, fixing a real bug: agents writing in parallel could break the hash chain and makeverifyreport tampering that never happened. (who did what) - 2026.07 —
v0.4.0on npm. One command to make docket ambient in a repo (docket install— context + hook + MCP, committed and shared); the loop becomes the full agent contract (goal/stop/budget); anddocket metricsreads the record back as your autonomy posture — auto-approve vs ask vs deny, longest unattended run, actions per intervention. (why this matters) - 2026.07 — The red-team program grows to 10,582 checks across six suites: adversarial phrasing, vague-target probes, 10,000 fuzzed targets, 239 tamper mutations, and a live hook-gate corpus — zero silent allows, zero fail-open. The matcher now splits compound targets clause by clause, so a consequence can't ride along with an allowed phrase.
- 2026.07 —
v0.3.0shipsdocket hook: the warrant as a Claude Code PreToolUse gate — allow/ask/deny enforced by the harness, not the prompt. Plus the deferred-consequence rule in the spec and the scheduled escape red-team family. - 2026.07 —
v0.2.1on npm: current README and CLI help ship in the package. - 2026.07 —
v0.2.0shipsdocket review: the record proposes warrant amendments; applying is always a human keystroke. - 2026.07 — OpenClaw and Hermes integrations, plus the full documentation site.
- 2026.07 —
v0.1.0: first public release — loops, warrants, hash-chained records, compile targets, MCP server.
Yesterday's failure was a bad answer: the model forgot everything, so you re-briefed it from scratch and corrected it in chat.
Today's failure is a bad action: agents use tools. A misread doesn't come back as a wrong paragraph — it goes out as a sent email, a filed ticket, a changed record.
It's already happened in the wild: in mid-2025, Replit's coding agent deleted a SaaS company's production database during an explicit code freeze — after being told, eleven times, in all caps, not to touch anything. Then it reported that rollback was impossible (it wasn't) and generated thousands of fake records to paper over the damage. Two failures in one incident: an action nobody permitted, and a record nobody could trust.
So the question that matters isn't "what does the AI know?" It's:
What exactly was the agent allowed to do — and can you prove it?
Docket makes the answer a file instead of a vibe.
- Delegation without babysitting. The boundaries are decided once, in
writing, under calm conditions — so the agent works unattended and stops
exactly where you said.
askstays rare and meaningful: across the red-team program, warranted work runs without a single unnecessary prompt (21/21), anddocket reviewretires the asks you keep approving. - Evidence you can hand to anyone. A client, a manager, a compliance review, a postmortem — the record answers "what was it allowed to do, and what did it do?" from a hash-chained file, not from memory. 239/239 tampering attempts detected in the eval.
- No vendor bet. The rules and the record are plain files in your repo, compiled to whichever assistant you use this quarter. A model switch is a recompile; deleting docket loses you nothing but the tooling.
Don't configure an assistant. Define a loop — one recurring task, wrapped in five layers:
┌───────────────────────────────────────────┐
│ one loop │
│ │
brief ────┤ what it must know before it starts │
procedure ────┤ how this job is done properly │
warrant ────┤ read / draft / change / send — and where │
│ it must stop and ask │
record ────┤ evidence of what it saw, did, skipped │
reserved ────┤ what stays with the human, always │
└───────────────────────────────────────────┘
Each loop is a single Markdown file. Prose where humans are good (brief, procedure), structure where tools are good (warrant, record, reserved):
---
name: insurance-appeal
description: Build the appeal, cite the policy — stop before send.
warrant:
read: [policy documents, denial letter, claim correspondence]
draft: [appeal letter, evidence summary]
send: []
ask: [contacting the insurer, requesting new records]
never: [accepting or rejecting a settlement]
reserved:
- signing and sending
record:
- every policy clause cited, with section numbers
- where the draft stopped and what a human must do next
---
# Brief
The denial reason code, the claim timeline, the appeal deadline…
# Procedure
Read the denial letter first. Answer the stated reason, not a general
sense of unfairness. Quote the policy both ways. Stop before send.Addy Osmani's Agentic Autonomy Levels makes the point cleanly: "Every run of an agent should be preceded by a contract that defines what it's trying to do" — goal, scope, non-goals, tools and permissions, stopping condition, evidence, escalation, budget. And "the autonomy level should follow the verification process, not the task name."
That contract is a loop file. Docket already carried most of it; a loop now names the whole thing:
| The contract asks | The loop answers |
|---|---|
| What outcome? | goal |
| What may it do, where must it ask, what's forbidden? | warrant (read/draft/change/send · ask · never) |
| When does it stop? | stop |
| What stays human? | reserved |
| What must it prove? | record |
| What's the ceiling on the run? | budget (tokens, attempts, parallelism…) |
goal: A reviewed fix on staging with tests green and a rollback plan — ready to ship.
stop:
- the failing test is now green on staging
- a change would touch production, or anything on the ask/never list
- the same fix has failed three times — hand back to a human
budget: { attempts: 3, parallelism: 1 }goal/stop/budget are descriptive — compiled into the agent's context
so it knows its own contract. Docket doesn't execute, so it can't count tokens
or evaluate a stop condition; enforcement of actions stays the warrant's job
(and the hook's). Writing the
contract down is what makes an autonomy level defensible — the agent, the
human, and any reviewer read the same thing.
And you can watch the posture, not assume it. docket metrics derives your
autonomy numbers straight from the record — no new data:
$ docket metrics
Warrant checks 7
allow 4 ██████████░░░░░░░░ 57% ran on its own
ask 2 █████░░░░░░░░░░░░░ 29% stopped for a human
deny 1 ███░░░░░░░░░░░░░░░ 14% hard stop
Autonomy
actions per intervention 2.3 proxy — checks ÷ (asks + denies)
longest unattended run 3 consecutive allows, no human stopThe record was always the evidence; now it's the dashboard for climbing the
autonomy ladder deliberately — the "calibrated autonomy" Osmani argues is
the mature posture. Add --json to wire it into CI.
You don't standardize on a client or set anything up per developer. Add docket to the repo, commit it, and every agent that touches the code — and everyone who clones — is under the same warrant and leaves the same record. This is the enterprise path: govern the repository, not each person's toolchain.
$ npm install -g docket-agent # or: npx docket-agent <command>
$ docket new deploy --template prod-hotfix # one rule file for a recurring task
$ docket install # wire it into the repo for every agent
✓ docket installed into .
CLAUDE.md · AGENTS.md · GEMINI.md · .cursor/rules/docket.mdc
.claude/settings.json · .mcp.jsondocket install sets up two layers together, all in files committed with the
repo:
- Context, for free. The compiled
CLAUDE.md/AGENTS.md/GEMINI.md/ Cursor rules are read automatically at the start of every session — Claude Code, Codex/ChatGPT, Gemini, Cursor, any MCP client, zero setup on anyone's end. - Enforcement, mechanical (one-time approval per developer). A Claude
Code PreToolUse hook in
.claude/settings.jsongates tool calls through the warrant (deny blocks, ask prompts, allow is silent), and.mcp.jsongives any MCP client the native tools. The hook routes by content and stays out of the way when no loop claims a call (pass-through); pin one loop with--loop, or--strictto ask on anything uncovered.
The two layers set up different expectations, and it's worth being precise:
the context travels instantly — everyone who clones is under the warrant
with zero setup. The enforcement hook asks each developer to approve the
committed hook once, the first time it runs; that prompt is Claude Code's own
safety gate (a cloned repo shouldn't run commands on your machine unattended —
the exact ambient-execution risk docket exists to catch). It's also merge-safe
(existing settings.json hooks and MCP servers are preserved), idempotent, and
zero-dependency. Commit .docket/ and the files above, and the whole team
inherits it on the next git pull.
Prefer to wire a single tool by hand? docket init → docket new →
docket compile --target <tool> --write does one target at a time; the
per-tool setup
covers each.
No template that fits? Bare docket new is a step-by-step creator: five
steps, one per layer, each explained as you answer. It previews the finished
file, asks before writing, then runs live allow/ask/deny checks against the
warrant you just wrote — the fastest way to feel how the spec works.
Ask the warrant before the agent acts:
$ docket check appeal draft "appeal letter"
ALLOW draft → "appeal letter"
"appeal letter" is within the draft warrant.
$ docket check appeal send "appeal email to the insurer"
ASK send → "appeal email to the insurer"
"appeal email to the insurer" is not listed under `send`.
Unlisted means ask — silence is never permission.
$ docket check appeal change "accepting a settlement"
DENY change → "accepting a settlement"
"accepting a settlement" matches a hard stop. The loop says this
never happens, with or without approval.That's an agent overreach prevented by a text file. And the default posture
is the important part: the warrant never granted send anything, so every
send asks — the agent doesn't need to anticipate the exact email to be
stopped by it. The same posture covers the database story: never: destructive commands in production is decided under calm conditions, and no in-the-moment
panic overrides it.
Matching is word-level, stemmed, and asymmetric: ask/never patterns
match fuzzily in both directions (accepting a settlement hits accepting or rejecting a settlement), while allow patterns match strictly — a vague
target like "email" can never inherit permission from a specific allow
entry like "status email to the team". A phrasing difference can cause an
unnecessary ask, never an accidental allow.
A send wearing a disguise is still a send. The newest failure class in the
suite is the scheduled escape — "queue the email for Friday", a git hook
planted in the repo, a CI job that acts next week: actions that look
contained now and detonate after the session, past every approval. The
shipped templates hard-stop them (scheduled or automated sending; git hooks, CI workflows, or scheduled jobs), and the
spec's rule is general: an action
classifies by where its consequences eventually land, not where the bytes
land first. Compound intent gets the same treatment: the matcher splits a
target on conjunctions and requires every clause to be warranted, so
"draft the appeal and send it" cannot ride the allow for "draft".
We red-team all of this, seven ways, on every CI build — 10,614 checks:
| Suite | Checks | Result |
|---|---|---|
| Behavior scenarios | 61 | 0 silent allows · 21/21 warranted work allowed |
| Adversarial phrasing — euphemism, compound intent, injection, homoglyphs | 42 | 42/42 contained |
| Vague-target probes ("email" vs "status email to the team") | 218 | 0 permissions inherited |
| Fuzzed targets, deterministic seed | 10,000 | 0 allowed |
| Record-tampering mutations | 239 | 239/239 detected |
| Hook gate, live binary vs hostile tool calls | 24 | 0 fail-open |
| Gateway gate, live binary vs hostile MCP tool calls | 30 | 0 fail-open |
Zero silent allows. Zero fail-open outcomes. Zero warranted work blocked.
Where a paraphrase weakens a hard stop, it weakens to ask — never to allow,
and the report says so in the open. Every invariant is enforced by
npm test; regenerate every number with npm run eval.
Exit codes are part of the contract (0 allow, 2 ask, 3 deny), so you can
gate hooks, scripts, and CI on the warrant directly.
Every warrant check and every piece of finished work lands in an append-only, hash-chained log — each entry commits to the one before it:
$ docket record add appeal \
--saw "policy §4.2, denial letter 2026-06-12" \
--did "drafted appeal citing §4.2(b), built evidence list" \
--stopped "before send — two claims need human verification"
✓ record #4 sha256:fd4394fc8cd4b288… by claude-code
$ docket record verify
✓ chain intact — 4 entries, every entry commits to the one before it
head: sha256:fd4394fc8cd4b288…Now edit one character of an old entry:
$ docket record verify
✗ chain broken at entry 4: entry 4 was modified after it was written
a record that can be edited quietly is not a recordA record that can be edited quietly is not a record. This one is a plain JSONL file you can read, grep, and commit — but not silently rewrite.
A hash chain can't see its own tail being cut off: delete the last ten entries
and what's left is a perfectly valid, ten-entries-shorter chain. Pinning the
head (verify --head <hash>) catches that on one machine. Signing it makes
the proof portable — something you hand to a client, an auditor, or a release:
$ docket record keygen # ed25519, kept outside the repo
$ docket record sign
✓ signed the record at 47 entries
head: sha256:fd4394fc8cd4b288…
→ .docket/attestations/000047-fd4394fc8cd4.json
$ docket record verify --attest --key <public key>
✓ record matches the attestation — signed at 47 entries, 12 appended since
key: pinned — this is the key you said to trustVerification checks the entry at the attested sequence number, not just the current head — so cutting ten entries and appending three more doesn't hide it:
✗ record does not match the attestation: entry 47 hashes to sha256:9c1e…,
but the attestation signed sha256:fd43… — the record was rewritten below
the signed pointTwo things it refuses to overstate. Signing a chain that doesn't verify is a
rubber stamp, so sign won't do it. And an attestation carrying its own public
key proves only that the attestation wasn't edited — anyone can generate a
key and sign anything. Without --key, verify says exactly that instead of
printing a green check that means less than it looks like:
key: not pinned — this proves the record is intact and the attestation
unedited, but not who signed it. Anyone can generate a key.One human running one agent needs no subject in the log — everything in it is the agent. That stops being true the moment you run three, in three worktrees, on the same repo. At merge time, "what was it allowed to do, and what did it do?" needs a who.
Every entry now carries one, stamped automatically:
$ docket record log
#7 2026-07-24 09:12Z deploy allow change → "src/api/rates.ts" (change: source files) ← claude-code @ hotfix-402
#8 2026-07-24 09:12Z deploy ask send → "Bash: gh pr merge" (default) ← claude-code @ hotfix-402
#9 2026-07-24 09:14Z deploy allow read → "test/rates.test.ts" (read: the repo) ← cursor @ wt-perf:perf-pass
$ docket metrics --by claude-code # one agent's posture, not the team averageby is resolved in precedence order — --by <agent>, then DOCKET_BY, then
a detected agent (Claude Code, Cursor, Gemini CLI, Codex, Aider, GitHub
Actions), then the OS user — alongside the git branch, the worktree name
when the write came from a linked one, and the harness's session id when it
supplies one (the Claude Code hook does). Fields with no honest value are
left out rather than filled with a placeholder that reads like a fact.
Two things worth being precise about, because attribution is easy to oversell:
byis self-reported. A process that can write to the record can claim any subject. This is provenance, not authentication.- But it can't be revised.
byis inside the hashed entry, so rewriting who an old entry blames breaks the chain at that entry. Whoever wrote it is stuck with what they claimed at the time — which is the property an audit actually needs.
And parallel agents no longer break the chain. Appending is
read-the-head-then-chain-to-it, so two agents writing at once could both chain
to entry 5 — after which verify reported "an entry was removed, added, or
reordered" on a log nobody had touched. Appends are now serialized across
processes (an exclusive lock file, stale locks broken after 10s, an error
rather than an unlocked write if it can't acquire one). A false tamper alarm
is worse than no alarm: it teaches people to disbelieve the real one. Five
agents × twelve writes each, landing simultaneously, is now a test.
Context locked inside one vendor's assistant is their context, not yours. Loops are the source of truth; assistant files are build artifacts:
$ docket compile --target claude --write # → CLAUDE.md
$ docket compile --target agents --write # → AGENTS.md (ChatGPT/Codex, Zed, …)
$ docket compile --target gemini --write # → GEMINI.md (Gemini CLI)
$ docket compile --target cursor --write # → .cursor/rules/docket.mdcSame loops, every tool. A model switch is a recompile, not a re-teach — try the new tool, point it at the same files, keep working.
Compiling every brief and procedure into the context file stops scaling around a handful of loops — the rules start crowding out the work. So rules scale on disk, not in context:
$ docket compile --index --target claude --write
✓ compiled index of 23 loops → CLAUDE.md--index compiles the protocol plus one line per loop — name,
description, and the loop's triggers — instead of the loops themselves.
The agent routes each task to its loop, then pulls just that loop in full:
$ docket match "draft an appeal for my denied claim"
1 candidate loop for "draft an appeal for my denied claim"
appeal Build the appeal, cite the policy — stop before send.
score 14 — name: appeal · trigger: denied claim, denial letter
$ docket match "wire funds to a vendor"
NO LOOP "wire funds to a vendor"
No loop covers this task. Work outside a loop defaults to askRouting is deterministic and scored — loop name, author-written triggers
phrases, warrant targets, description overlap — and it fails closed: no
match doesn't mean "best guess", it means stop and ask, exit code 2,
same as the warrant. And enforcement never needed context residency at all:
the warrant check runs outside the model and injects the one matched rule
exactly when it becomes relevant. What stays resident is a table of
contents; the window holds one open chapter; the checker never forgets any
of it.
docket mcp is a zero-config MCP server. Add it to Claude Code:
$ claude mcp add docket -- npx docket-agent mcpor to any MCP client:
{ "mcpServers": { "docket": { "command": "npx", "args": ["docket-agent", "mcp"] } } }The agent gets five tools:
| Tool | What it does |
|---|---|
docket_list_loops |
discover your loops |
docket_match_loop |
route a task to the loop that covers it — ranked, fail-closed |
docket_loop_context |
pull a loop's five layers before starting |
docket_warrant_check |
allow / ask / deny, before acting — auto-logged |
docket_record |
add a verifiable record entry when it finishes or stops |
Warrant checks made by the agent land in the record too. "Did the agent even ask?" becomes a grep.
Compiled context and MCP tools work when the agent cooperates. docket hook claude doesn't need it to. Wired as a Claude Code PreToolUse hook, every
intercepted tool call is checked against the warrant by the harness —
docket's verdicts map onto Claude Code's permission decisions, whether or not
the model read a word of your rules:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash|Write|Edit|mcp__.*",
"hooks": [
{ "type": "command", "command": "npx -y docket-agent hook claude --loop repo-work" }
]
}
]
}
}Put that in .claude/settings.json, scope the matcher to the tools you
want gated, and the warrant is no longer advice. deny blocks the call and
tells the model why; ask makes Claude Code prompt you; allow stays
silent — docket only ever tightens the gate, never bypasses Claude Code's
own prompts. Lookup and edit tools map to read/change; anything docket
doesn't recognize — Bash, MCP tools, tools that don't exist yet — falls to
send, the verb whose allow list most loops keep empty on purpose, so an
unknown tool asks rather than slips through.
Pin one loop with --loop <name>; drop it to route each call by content
(add --strict to ask when no loop claims a call). Once you name a loop or
pass --strict, every failure mode — bad payload, missing project, misnamed
loop — fails closed to ask: a gate you asked for that fails open is not a
gate. Gated calls land in the record (via: "hook"), so the asks you keep
approving surface in docket review — the gate teaches the warrant what to
say next.
The hook gates one harness. It works beautifully and it only works in Claude Code — which is fine until you notice where an agent's most consequential actions actually go. They don't go through the harness's file tools. They go out over MCP: to a mail server, a ticket tracker, a cloud API, a payments provider.
docket intercept gates that side. Wired into the
Docker MCP Gateway as an interceptor,
every tools/call from any client to any server is checked against the
warrant before it runs:
$ docker mcp gateway run \
--interceptor 'before:exec:docket intercept --loop deploy'or, with no Node on the host, as a container:
$ docker mcp gateway run \
--interceptor 'before:docker:ghcr.io/shahcolate/docket-agent intercept --loop deploy'The gateway already intercepts what a gateway can know generically — secrets in payloads, OAuth failures. Docket adds what it can't: whether this action was permitted, for this job, by the person accountable for it. A gateway can tell that a payload contains an API key. Only the warrant can tell the authorized appeal email from the unauthorized one.
One difference from the hook, stated plainly, because it is a real
constraint and not a footnote: ask blocks — it does not ask. A gateway has
no human on the connection to prompt. At a PreToolUse hook, ask raises a
prompt someone can approve; here there is nobody, so the call does not run and
the message tells the model to get approval out of band. That is strictly
tighter than the hook, which is the only direction docket is ever allowed to
differ across surfaces — but it means a gateway-fronted workflow stops dead on
anything unlisted rather than waiting for a yes. Write the warrant knowing
that.
The gateway's contract inverts the hook's in a way worth naming, because it
changes what "a bug" means: stdout empty means the tool runs. So an
interceptor that crashes, that prints a warning to stdout, or that emits
anything the gateway can't unmarshal has allowed the call. All three are
counted as fail-open in the eval,
not filed as polish for later — 20 hostile tool calls (mail, merges, refunds,
DROP TABLE, planted git hooks, a wire transfer, padding attacks against the
target's length cap), 0 allowed, 0 crashes.
Every tool behind a gateway is third-party and arbitrary, so docket does not
guess a verb from a tool name — search_and_purge reads like a read.
Everything defaults to send, the verb whose allow list loop authors keep
shortest on purpose. --action read overrides that for a gateway you know
fronts read-only servers, and it's a real widening; the docs say so.
Full setup, including a runnable Compose file: examples/mcp-gateway.
A platform team has rules that hold everywhere: never commit a secret, never run destructive commands in production, always ask before spending money. Copying those into forty loop files gives you forty places to forget them — and no way to answer "is that rule actually in force everywhere?" without a grep and a hope.
---
name: deploy
extends: org-baseline # ← the floor, in one file
warrant:
change: [staging environment, feature branches]
---The merge rule is one sentence: every list is a union, parent first. That's
all of it, and it's safe for a structural reason rather than a careful one —
the verdict order is never, then ask,
then allow. A union can only add entries, and the two lists consulted first
are the restricting ones. So:
- A child can't delete a parent's
never. It's still there, still checked first. - A child that allows something the parent asks about doesn't win — the
parent's
askis reached first, and the child's entry is simply never consulted.
A child may widen only into space the baseline left open, and a baseline
closes space by writing ask or never — not by keeping its own allow lists
short. There is deliberately no override escape hatch: a baseline a child can
opt out of is a suggestion, and this field exists precisely because a
suggestion wasn't working.
And when a rule fires, the verdict says whose it was:
$ docket check deploy change "drop the production users table"
DENY change → "drop the production users table"
"drop the production users table" matches a hard stop. The loop says this
never happens, with or without approval. This rule is inherited from the
"org-baseline" baseline.That from lands on the record too. "Which policy stopped this?" is the
first question at an audit, and it shouldn't take re-reading four files.
Baselines are marked abstract: true: real policy, but nothing routes to them
and they never show up as work. docket new baseline --template org-baseline
ships a starting one. Budgets inherit too, with a floor — a numeric limit
merges to the minimum, so a child can lower a ceiling and never raise one.
extends: makes one file govern forty loops. It doesn't help when the forty
loops are in forty repos. For that, put the policy in the registry you already
run:
$ docket policy push ghcr.io/acme/loops:baseline
pushing 1 loop → ghcr.io/acme/loops:baseline
6b6359544a5a org-baseline.loop.md
✓ pushed ghcr.io/acme/loops:baseline
digest: sha256:e568d3f76395…Each loop is its own layer, annotated with its filename — so it's individually
addressable, readable in any registry UI, and oras pull-able by someone who's
never heard of docket. Registries already have what policy distribution needs
and a git submodule doesn't: immutable digests, tags that move deliberately,
access control per namespace, and a retention policy somebody else operates.
It's how the Docker MCP Catalog ships its server list, for the same reasons.
Pulling is the dangerous direction, and the command is built like it. A
loop file isn't data — it's the thing that decides what your agents may do.
Fetching one off the network and dropping it into .docket/loops/ is
structurally the exact failure docket exists to prevent. So pull shows you
what changes and then asks:
$ docket policy pull ghcr.io/acme/loops:baseline
ghcr.io/acme/loops:baseline
digest sha256:e568d3f76395…
+ new org-baseline.loop.md (abstract)
never 6 · ask 4
install 1 loop file into .docket/loops? [y/N]docket policy inspect reads a policy back without installing anything —
including every never rule, so you can see what you'd be agreeing to. Digests
are verified before a byte is written; a publisher-chosen filename is validated
against the loop-name grammar (a layer titled ../../../etc/cron.d/evil.loop.md
is refused, and that's a test); an existing loop is never silently replaced; and
what you installed lands on the record with its digest.
What extends: deliberately cannot do is name a registry. A warrant check
must never depend on the network — the moment it does, the interesting question
stops being "what is this agent allowed to do" and becomes "what happens to
the gate when the registry is down, or slow, or serving a tag that moved under
us", and every honest answer to that is worse than the problem it solves. So
policy is pulled explicitly, vendored into the repo, and committed. docket policy pull is a supply-chain step, not a runtime dependency, and the warrant
is always evaluated from local files a reviewer can read in a diff.
Run your agent in one — genuinely, do. Sandboxes (containers, egress filters, read-only mounts) bound damage: what the process can physically reach. Docket bounds authority: what the agent was allowed to do, and whether you can prove what it did. A sandbox cannot tell the authorized appeal email from the unauthorized one — both are legitimate HTTPS through the proxy. The warrant can, and the record shows which one happened.
The two layers meet at the failure that scares us most. A red-team pass on
an agent sandbox found the agent could plant a git hook in a submodule that
would have executed on the host, days after the session ended. The
sandbox was secure; the escape was scheduled. Isolation is a boundary in
space; that attack is a boundary in time. It is now a
scenario family in our eval suite, a never in the
shipped templates, and a rule in the spec.
So run both — and mount the record out, because a sandbox is designed to be disposable and an audit trail is not:
$ docker run --rm -it -v "$PWD:/work" -v "$PWD/.docket:/work/.docket" your-agent-imageThe rules travel in (they're just repo contents), the evidence travels out.
Parallel sandboxes are safe on one shared record: appends are serialized across
processes, and every entry is attributed. When the run ends, docket record sign makes the head portable.
docs/sandboxes.md works the whole composition through — four boundaries (isolation, egress, authority, evidence), what each one actually catches, where the seams are, and what none of them give you.
And no, the answer to agent risk is not approving every command — airport
security for Bash scripts is the failure mode, not the goal. The warrant
pre-decides allow and deny under calm conditions so that ask stays
rare and means something, and docket review retires the asks you keep
approving. The gate's job is to be silent until the moment silence would
have been permission.
Docket is deliberately scoped, and it never invents a verdict for work you didn't write a loop for. Retrieval fails open to the human; the warrant fails closed. What "no loop covers this" means depends on where you are:
| Surface | No loop covers it | Why |
|---|---|---|
| The hook, default | pass-through — docket stays silent, your normal permission flow decides | docket governs what you wrote loops for; it doesn't seize unowned work |
The hook, --strict |
ask — a human approves anything outside the loops | opt-in "nothing moves without a loop" posture |
| Inside a matched loop, unlisted action | ask (the default verdict) | unlisted means ask — silence is never permission |
docket match <task> |
exit code 2 ("no loop covers it") | so scripts and CI can branch cleanly on "unrouted" |
And if you want docket out of something entirely: it never executes
anything — it only ever returns a verdict and appends to the record, so
there's nothing to "turn off" mid-action. Engagement is opt-in per surface —
don't wire the hook and it only answers when explicitly asked; scope the
hook's matcher to the tools you actually want gated; and outside a
.docket project a bare hook costs nothing.
OpenClaw injects your workspace's AGENTS.md
into the agent's system prompt at the start of every session — so compile
straight into the workspace (fitting, given the story that opens this README):
$ cd ~/.openclaw/workspace
$ npx docket-agent init
$ npx docket-agent new followup --template client-follow-up
$ npx docket-agent compile --target agents --writeDocket only manages its own marked block inside AGENTS.md — your existing
rules, SOUL.md, and the rest of the workspace stay untouched. OpenClaw can
also run the MCP server for native checks and record entries: add docket
as an MCP server in your OpenClaw config with
command: npx, args: ["-y", "docket-agent", "mcp", "--dir", "~/.openclaw/workspace"].
Hermes (Nous Research)
reads AGENTS.md context files too — run the same three commands in the
directory Hermes works from. For native tools, add docket under the MCP
servers section of ~/.hermes/config.yaml:
docket:
command: npx
args: ["-y", "docket-agent", "mcp", "--dir", "/path/to/your/project"]Any other agent that reads AGENTS.md, CLAUDE.md, GEMINI.md, or speaks
MCP gets the same treatment — one loop file, every agent under the same
warrant.
The full guide — concepts, loop-file reference, the verdict algorithm, matching semantics, record internals, CLI reference, and per-tool setup — lives at the docs site. The normative format definition is the Loop File Spec.
docket new <name> interviews you:
- What must it know before it starts?
- How is this work supposed to be done?
- What may it do without asking?
- Where does it have to stop?
- What evidence must it leave behind?
Unwritten answers get guessed at. Written answers get enforced — the questions are the schema: brief, procedure, warrant, reserved, record.
The record knows where the warrant chafes: every time the agent hit an
unlisted action, a default-ask was logged. docket review mines those and
proposes the exact amendments:
$ docket review
2 proposed amendments — from repeated asks in the record
1. appeal — allow read: "state insurance regulations" (asked 4×)
2. appeal — allow draft: "timeline summary" (asked 2×)
allow read: "state insurance regulations" in appeal? [y/N] y
✓ appeal: read now covers "state insurance regulations"Three rules keep it honest: the analysis is automatic but applying is
always a human keystroke (an agent that widens its own permissions is the
exact failure docket exists to prevent — it's in our red-team suite);
anything on the ask or never lists is never proposed, however often
it recurs — those are policy, not friction; and every approved amendment is
appended to the record, so even the evolution of the rules is auditable.
Run it weekly, or wire it into a cron — the proposals wait for you.
Nine templates, each a complete worked example (docket templates):
| Loop | The gist |
|---|---|
org-baseline |
the floor every other loop inherits — abstract, extends-only |
prod-hotfix |
diagnose and fix on staging — production asks, destructive commands never |
insurance-appeal |
build the appeal and the evidence packet, stop before send |
client-follow-up |
promises made, approved language, tone — approval rules included |
travel-morning |
your walking tolerance and food rules, not a guidebook's |
weekly-planning |
propose the week and its tradeoffs; change nothing |
marketing-brain |
marketing memory that compounds; confident vs. unsupportable, in writing |
ticket-handoff |
tasks a stranger can pick up cold: source, owner, status, blocker, warrant, record |
cross-tool-memory |
one context readable from Claude / GPT / Kimi / Codex |
Selling this honestly means saying where the edges are:
- Not a sandbox. Docket bounds authority and proves what happened; a sandbox bounds what the process can physically reach. Run both — they compose, and the record should be mounted out of the sandbox rather than thrown away with it.
- Not another agent framework. There is no runtime, server, or account to adopt. It's a file format, a checker, and a log — the layer under whichever agent you already use.
- Not a magic cage. A cooperative agent follows the compiled rules; the
hook and the
gateway interceptor enforce them
mechanically where you wire them; and either way, every check lands on the
record. Each layer is exactly as strong as it claims, and no stronger — the
interceptor gates the MCP path, not a shell inside the sandbox reaching for
curl. - Not finished. This is v0.5.x, and the spec may still break before 1.0
(loop files carry a
versionfield for exactly that reason). What won't move: unlisted means ask, failures land on the human, and the record stays tamper-evident.
- Plain files, forever. Markdown + JSONL in your repo.
grepworks,git diffworks, deleting docket loses you nothing but the tooling. - Zero dependencies.
node >= 18and nothing else. The tool that holds your agent's permissions should have a supply chain you can read in an afternoon. - Unlisted means ask. The default verdict is the safety property.
- Describe, don't execute. Docket is not another agent framework — it's the layer under whichever agent you already use. Models stay interchangeable; the context stays yours.
Read the Loop File Spec — it's short on purpose.
-
— shipped asdocket checkas a Claude Code PreToolUse hookdocket hook claude -
Per-agent / worktree attribution in the record (a— shipped in v0.5.0, with the concurrency fix it neededby:field) -
Signed record heads (attest the chain tip, share the attestation)— shipped asdocket record sign -
Loop inheritance (— shipped in v0.6.0extends:) for team baselines -
The warrant as an MCP gateway policy layer— shipped asdocket intercept -
Loop distribution as an OCI artifact— shipped asdocket policy push/pull/inspect - Autonomy-levels guide — mapping loops and readiness to L0–L5, so teams pick a level by its verification, not the task name
- Record export → human-readable work summaries
- Adapters: OpenAI custom instructions, Windsurf
The spec is deliberately small — issues that argue about the warrant
algorithm are the best kind. npm test runs the whole suite with zero
setup. The fastest ways in: a new starter template, or a
red-team scenario that breaks the matcher — if it finds a silent allow,
it goes straight into the eval suite with your name on
it. See CONTRIBUTING.md.
MIT © docket contributors
Models come and go. Your context shouldn't.