How docket works
Everything on this page is implemented, tested, and reproducible from the repo. The normative format definition is the Loop File Spec; this page is the guided tour.
What docket is
AI agents act now — they send email, move meetings, file tickets, change records. When an agent misreads you, you don’t get a wrong paragraph back; you get a wrong action with your name on it.
Docket is the permission layer and the paper trail. You describe each recurring task in one Markdown file (a loop). Before the agent acts, it checks the loop’s warrant and gets a deterministic verdict — allow, ask, or deny. After it acts (or stops), it appends what happened to a hash-chained record that exposes any tampering.
Three design commitments shape everything else:
- Unlisted means ask. The default verdict is the safety property. An agent that encounters something you didn’t anticipate has, by definition, encountered something that needs a human.
- Plain files, no platform. Rules and record live in your repo as Markdown
and JSONL.
grepworks,git diffworks, deleting docket loses you nothing but the tooling. Zero runtime dependencies. - Describe, don’t execute. Docket is not an agent framework. It’s the layer underneath whichever agent you already use — Claude, ChatGPT/Codex, Gemini, Cursor, OpenClaw, Hermes, or anything that speaks MCP.
Quickstart
npm install -g docket-agent # or use npx docket-agent <command> cd your-project docket init # creates .docket/ docket new appeal --template insurance-appeal docket compile --target claude --write # or agents / gemini / cursor
Then try the warrant:
$ docket check appeal draft "appeal letter" ALLOW "appeal letter" is within the draft warrant. $ docket check appeal send "appeal email to the insurer" ASK unlisted means ask — silence is never permission. $ docket check appeal change "accepting a settlement" DENY matches a hard stop. Never happens, with or without approval.
Run docket new <name> without a template to get the five-question
interview instead, and docket templates to list all eight starters.
Make it ambient in a repo
One command wires docket into the whole repo so every agent that touches it is under the warrant — context and enforcement, committed with the code:
docket install # compile context for every agent + hook + MCP configIt compiles the loops into CLAUDE.md, AGENTS.md,
GEMINI.md, and Cursor rules (read automatically at session start — zero
setup for anyone who clones), writes a Claude Code PreToolUse hook into
.claude/settings.json (the mechanical gate), and
adds a .mcp.json for native tools. It’s merge-safe (existing hooks and
MCP servers are preserved) and idempotent — add a loop, re-run, the context
refreshes. Flags: --loop <name> to pin one loop, --strict
to ask on uncovered calls, --no-hook / --no-mcp for
context-only, --targets claude,agents to narrow the context files.
Loops & the five layers
Don’t configure an assistant. Bound one recurring task — the insurance appeal, the client follow-up, the weekly plan. Each loop wraps that task in five layers:
| Layer | Question it answers | Lives in |
|---|---|---|
| brief | What must the agent know before it starts? The context that changes the answer: people, history, constraints, standards, past decisions. | Markdown body |
| procedure | How is this job done properly? Which sources count, what finished looks like, the known ways it goes wrong. | Markdown body |
| warrant | What may it do without asking? Read / draft / change / send lists, plus always-ask and never. | YAML frontmatter |
| record | What evidence must it leave behind when it finishes or stops? | YAML frontmatter |
| reserved | What stays with the human, always? Accounts, secrets, spending, final sign-off. | YAML frontmatter |
Prose layers live in the body because humans write and diff prose well; machine layers live in frontmatter because tools enforce structure well.
The loop is an agent contract
Addy Osmani’s Agentic Autonomy Levels argues that “every run of an agent should be preceded by a contract” — goal, scope, permissions, stopping condition, evidence, budget — and that “the autonomy level should follow the verification process, not the task name.” A loop is that contract. Three optional frontmatter fields complete it:
| Field | Answers | Enforced? |
|---|---|---|
| goal | The outcome to reach — not an activity. | descriptive (in context) |
| stop | The conditions that end the run; the agent halts at any of them. | descriptive (in context) |
| budget | The ceiling on the run — tokens, attempts, parallelism, time. | descriptive (in context) |
goal/stop/budget are compiled into the agent’s
context so it knows its own contract, but docket doesn’t execute — it can’t count tokens
or evaluate a stop condition. Enforcement of actions stays the warrant’s job;
these bound the run, and belong to whatever harness runs the agent. Writing the
contract down is what makes an autonomy level defensible.
Writing a loop file
A loop is a single file, <name>.loop.md, in
.docket/loops/:
--- name: insurance-appeal description: Build the appeal, cite the policy — stop before send. triggers: - insurance appeal, appeal a denial - denied claim, denial letter warrant: read: [policy documents, denial letter, claim correspondence] draft: [appeal letter, evidence summary] change: [] 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. Stop before send.
Rules the parser enforces
namemust be lowercase letters, digits, and dashes — and must match the filename, or record entries would point at a loop that can’t be loaded. Mismatches are rejected.- Headings named exactly
BrieforProcedure(any of#/##/###, case-insensitive) open the prose layers. Every other line — subheadings, code fences, comments inside fences — is content and stays with its section. Nothing you write is silently dropped. - Frontmatter is a deliberately small YAML subset: nested maps, lists of scalars, quoted strings, numbers, booleans. No anchors, no multi-line scalars — a grammar small enough to audit is part of the security posture.
- A
versionthe tool doesn’t understand is rejected, never silently misread. triggersis optional routing metadata — phrases that mark a task as this loop’s job. It never grants permission; it only helps routing find the loop.
The warrant
Four verbs, in escalating order of consequence:
| Verb | Meaning |
|---|---|
| read | look at it |
| draft | produce it, but it goes nowhere on its own |
| change | mutate state that stays inside the sandbox |
| send | consequences leave the sandbox: email, publish, file, deploy, pay |
Plus two cross-cutting lists: ask (always needs human approval,
whatever the verb) and never (does not happen, even with approval — you,
under calm conditions, pre-deciding what no in-the-moment persuasion may undo).
scheduled or automated sending and
git hooks, CI workflows, or scheduled jobs.The verdict algorithm
Given a loop, an action verb, and a target (a plain-language description of what the action touches), the verdict is the first rule that matches:
- target matches
never→ deny - target matches
ask→ ask - target matches the action’s own list → allow
- otherwise → ask
Matching semantics
Matching is case-insensitive and asymmetric by design: a phrasing difference may cause an unnecessary ask; it must never cause an accidental allow.
- A pattern containing
*is a glob over the whole target. - Commas,
or, andandsplit a pattern into alternatives (secrets, tokens, or passwordsis three patterns). - Words compare under light stemming (
quotes≈quote,contacting≈contact), and filler words (a,the,anything…) don’t count. - Cautious mode (ask/never): substring or word-subset match in either direction — ambiguity escalates to you.
- Strict mode (allow lists): the target must cover the pattern.
The vague target
"email"never inherits permission from the specific entry"status email to the team". - An all-filler pattern like
never: [anything]matches everything — its plain meaning.
Exit codes are the contract
docket check exits 0 for allow, 2 for ask, 3 for
deny (1 for usage errors) — so shell hooks, wrappers, and CI can gate on the warrant
directly.
The record
Every warrant check and every piece of finished work is appended to
.docket/record.jsonl — one JSON object per line, append-only. Three
entry kinds:
| Kind | What it captures |
|---|---|
| check | a warrant check with its verdict and the rule that fired — written automatically. “Did the agent even ask?” becomes a grep. |
| note | work evidence: saw, did, skipped, stopped, note |
| amend | a human-approved warrant widening from docket review — rule changes are evidence too |
The hash chain
Each entry commits to the previous one:
hash = sha256(prev + canonical(entry)). Any edit, deletion, insertion, or
reordering breaks the chain at a specific entry, and
docket record verify names it.
One case a chain alone can’t see: cutting off the tail leaves a valid
shorter chain. So verify prints the current head hash — pin it somewhere
the log can’t reach (a password manager, a commit message, another machine), then
docket record verify --head <hash> detects truncation too.
Review: iteration with a human veto
The record already knows where the warrant chafes: every unlisted action the agent
was stopped on is a logged default-ask. docket review clusters the
repeats 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 guardrails keep the automation honest:
- Applying is always a human keystroke. An agent that widens its own permissions is the exact failure docket exists to prevent — it’s a scenario in the red-team suite.
- Deliberate policy is never proposed. Targets matching the loop’s
askorneverlists don’t appear, however often they recur. - Every approved amendment lands on the record as an
amendentry — the evolution of your rules is itself auditable.
Flags: --min N (repeat threshold, default 2), --loop name
(one loop), --yes (apply all — for when you run it in a script
or cron). After approving, recompile so your tools see the new warrant.
Metrics: your autonomy posture, from the record
Because every check carries its verdict, the record is enough to report how much
the agent runs on its own versus stops for a human — no new data collected.
docket metrics reads it back:
$ 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 stop warrant amendments 0 human-approved widenings (docket review)
It reports the auto-approve / ask / deny split, the longest unattended run
(consecutive allows with no human stop), a proxy for actions-per-intervention, and a
per-loop and per-channel breakdown. Every number is exact from the hash chain except
the labeled proxies — the record measures actions and verdicts, not wall-clock or
intent. This is the dashboard Addy Osmani argues teams need to climb the autonomy
ladder deliberately toward “calibrated autonomy.” Add --json to
gate CI on it, or --loop <name> to scope it.
Compiling to your tools
Loops are the source of truth; assistant context files are build artifacts:
| Target | Writes | Read by |
|---|---|---|
claude | CLAUDE.md | Claude Code |
agents | AGENTS.md | ChatGPT/Codex, OpenClaw, Hermes, Zed, … |
gemini | GEMINI.md | Gemini CLI |
cursor | .cursor/rules/docket.mdc | Cursor |
raw | stdout | anything else |
Docket writes between <!-- docket:begin --> /
<!-- docket:end --> markers and manages only that block —
your hand-written content in the same file is never touched. Recompiling is
idempotent; loop content that quotes the markers is neutralized so it can’t corrupt
the block; an orphaned marker is an error, never silent data loss.
Because every target renders from the same loops, moving to a new tool is a recompile, not a re-teach.
Routing & context scale
The full compile puts every brief and procedure in the agent’s context on every turn. That stops scaling around a handful of loops — the rules start crowding out the work. The fix is architectural: rules scale on disk, not in context.
$ docket compile --index --target claude --write
✓ compiled index of 23 loops → CLAUDE.md--index compiles the same managed block in tiers instead of in
full:
| Tier | Resident? | Grows with |
|---|---|---|
| protocol | always | nothing — find the loop, load it, check the warrant, ask when uncovered |
| index | always | one line per loop: name, description, triggers |
| active loop | on demand | only the task at hand — docket compile --loop <name> or docket_loop_context |
| enforcement | never | the warrant check runs outside the model; its verdict carries the one matched rule into the conversation exactly when it matters |
docket compile prints a token estimate and suggests
--index when the full render grows past a few thousand tokens.
The two modes share markers, so switching replaces the block instead of stacking
a second one.
Which loop covers this task?
With only an index resident, something has to answer the routing question — deterministically:
$ 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 askScoring is lexical and integer-weighted, reusing the warrant’s cautious matcher: the loop name read as a phrase (+5), each author-written trigger (+4), matching warrant targets (+1 each, capped), shared description words (+1 each, capped). Candidates need a score of 3; the top few are returned for the agent — or you — to make the final pick. One shared word like “email” never routes on its own.
Two rules matter more than the weights:
- The asymmetry inverts at routing time. The warrant matches allow-entries strictly because a false allow is an incident. Routing matches generously because a false candidate costs one extra index line — and a routing miss is still caught downstream by the warrant.
- Retrieval fails closed. No match doesn’t mean “best guess” — it means
no loop covers this task, ask the human.
docket matchexits 2 on no coverage, the same exit as an ask verdict, so hooks can gate on it.
The MCP server
docket mcp is a zero-config MCP server (stdio, newline-delimited
JSON-RPC). MCP hosts often launch servers with a working directory far from your
project, so pass --dir /path/to/project (or set
DOCKET_DIR); the server always answers initialize and
reports a missing project as a tool error instead of dying silently.
| Tool | What the agent gets |
|---|---|
docket_list_loops | discover the loops you’ve defined |
docket_match_loop | route a task (in plain words) to the loop that covers it — ranked candidates with why, or a fail-closed “no loop covers this, ask” |
docket_loop_context | a loop’s five layers, before starting work |
docket_warrant_check | allow / ask / deny for a proposed action — auto-logged, with explicit STOP instructions on ask/deny |
docket_record | append evidence: saw / did / skipped / stopped / note |
Checks the agent makes over MCP land on the record with via: "mcp" —
distinguishable from human CLI checks forever.
Hooks: hard enforcement
Compiled context tells the agent the rules; MCP makes checking cheap. For the tool
calls you actually fear, make the warrant mechanical: docket hook
claude plugs into Claude Code’s permission system as a PreToolUse hook. Add to
.claude/settings.json:
{
"hooks": {
"PreToolUse": [
{ "matcher": "Bash|Write|Edit",
"hooks": [{ "type": "command", "command": "npx docket-agent hook claude" }] }
]
}
}Every matched tool call is piped through the warrant before it runs:
| Verdict | What Claude Code does |
|---|---|
| deny | the call is blocked; the loop, rule, and reason go back to the model |
| ask | Claude Code prompts you before the call runs |
| allow | silence. Docket emits no decision on allow — it only ever tightens the gate, never bypasses Claude Code’s own permission prompts |
How calls map to the warrant
- Read-shaped tools (
Read,Grep,WebFetch, …) check asread; file-writing tools (Write,Edit, …) aschange. Everything else —Bash, MCP tools, tools that don’t exist yet — checks assend, the most consequential verb, so unknown tools fall toward ask, never toward allow. - The target is the human part of the input (
Bash: git push origin main,Write: appeal-letter.md), so your warrant patterns match it in plain words. - Without
--loop, each call is routed with the same scoring asdocket match; when no loop claims the call, the hook stays silent and Claude Code’s own permissions decide. Pin one loop with--loop <name>, or add--strictto turn no-coverage into an ask. - Every check lands on the record with
via: "hook"— enforcement and evidence in one move. A bare hook (no--loop, no--strict) in a project without a.docketdirectory costs nothing. But once you pin a gate —--loopor--strict— every failure mode (a bad loop name, non-JSON stdin, a missing project) fails closed to ask: a gate you asked for never fails open.
Per-tool setup
The pattern is the same everywhere: init → new →
compile into the file that tool already reads. Sixty seconds each.
Claude Code
npx docket-agent init npx docket-agent new followup --template client-follow-up npx docket-agent compile --target claude --write # optional — native checks + record via MCP: claude mcp add docket -- npx docket-agent mcp # optional — hard enforcement: wire `docket hook claude` into # .claude/settings.json as a PreToolUse hook (see “Hooks” above)
ChatGPT · Codex
npx docket-agent compile --target agents --write # → AGENTS.mdAGENTS.md is an open convention — the same compile covers Zed, Amp, and other tools that read it.
Gemini CLI
npx docket-agent compile --target gemini --write # → GEMINI.mdGemini CLI loads GEMINI.md as project context on launch. Running Claude and Gemini side by side? Compile both targets — one rule file governs both.
Cursor
npx docket-agent compile --target cursor --write # → .cursor/rules/docket.mdcOpenClaw
OpenClaw injects the workspace’s AGENTS.md into the system prompt at
the start of every session — so run docket inside the workspace:
cd ~/.openclaw/workspace npx docket-agent init npx docket-agent new followup --template client-follow-up npx docket-agent compile --target agents --write
Docket manages only its own marked block — your existing AGENTS.md rules,
SOUL.md, and the rest of the workspace stay untouched. For native checks, add docket
as an MCP server in your OpenClaw config:
command: npx, args: ["-y", "docket-agent", "mcp", "--dir",
"~/.openclaw/workspace"].
Hermes (Nous Research)
Hermes reads AGENTS.md context files — run the same three commands in
the directory Hermes works from, then optionally register the MCP server in
~/.hermes/config.yaml under your MCP servers section:
docket: command: npx args: ["-y", "docket-agent", "mcp", "--dir", "/path/to/your/project"]
Hermes builds skills from experience; docket keeps the experience inside the warrant, on the record.
Any other MCP client
{ "mcpServers": { "docket": {
"command": "npx",
"args": ["-y", "docket-agent", "mcp", "--dir", "/path/to/project"] } } }CLI reference
| Command | Does |
|---|---|
docket init | create .docket/ here (--dir, --quiet) |
docket install | make docket ambient in the repo: compile context for every agent, wire the Claude Code PreToolUse hook, add MCP config (--loop, --strict, --no-hook, --no-mcp, --targets); merge-safe and idempotent |
docket new <name> | create a loop — interactive five-question interview on a TTY, --template <t> to start from a starter, --blank for a scaffold |
docket templates | list the eight starter loops |
docket list | list your loops |
docket show <loop> | print a loop’s five layers |
docket match <task…> | which loop covers this task? — ranked with why (--limit N); exits 0 matched / 2 no coverage |
docket check <loop> <verb> <target…> | verdict + rule; exits 0/2/3; logged unless --no-record; --quiet prints the bare verdict |
docket record add <loop> | append evidence: --saw --did --skipped --stopped --note |
docket record log [loop] | show recent entries (--n 20) |
docket record verify | verify the chain end to end; --head <hash> pins against tail truncation |
docket metrics | autonomy posture from the record: auto-approve / ask / deny split, longest unattended run, actions per intervention, per-loop & per-channel (--loop, --json) |
docket review | propose warrant updates from repeated asks (--min, --loop, --yes) |
docket compile | --target claude|agents|gemini|cursor|raw, --write to update the file in place, --loop to preview one loop on stdout, --index to compile the routing table instead of full loops |
docket mcp | run the MCP server (--dir or DOCKET_DIR to locate the project) |
docket hook claude | Claude Code PreToolUse hook: deny blocks, ask prompts the human, allow stays silent (--loop pins a loop, --strict asks on no coverage, --dir locates the project); with a gate pinned, every failure mode fails closed to ask |
The red-team eval
The claim “fuzzy only toward caution” is tested, not asserted — 10,582 checks
across six suites, all deterministic, all reproducible with npm run eval,
every invariant enforced in CI:
| Suite | Checks | Result |
|---|---|---|
| Behavior scenarios — real agent-overreach incidents, incl. scheduled escapes | 61 | 0 silent allows · 21/21 warranted work allowed |
| Adversarial phrasing — euphemism, compound intent, injection, homoglyphs | 42 | 42/42 contained |
| Vague-target probes — every proper subset of every allow pattern | 218 | 0 permissions inherited |
| Fuzzed targets — seeded, vocabulary disjoint from every allow entry | 10,000 | 0 allowed |
| Record tampering — edits, rehashes, deletions, reorders, forgeries, truncations | 239 | 239/239 detected with pinned head; 197 by the chain alone |
| Hook gate — the live binary vs hostile PreToolUse payloads | 22 | 14 hostile calls, 0 allowed · 3/3 benign allowed · 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. If any invariant regresses, the build fails
(test/scenarios.test.js, adversarial, properties,
tamper, hookgate). The full tables are in
eval/REPORT.md.
FAQ
Does docket execute anything?
No. It describes and constrains work; your agent does the work. That’s deliberate — it means docket works with every agent, including ones that don’t exist yet.
What stops the agent from just ignoring the rules?
Three layers, in increasing strength: the compiled context tells it the rules
(models follow explicit written boundaries far better than vibes); the MCP tools make
checking cheap and logged, so skipping the check is visible in the record; and
docket hook claude makes it mechanical —
Claude Code pipes tool calls through the warrant before they run, blocking on deny
and prompting you on ask. On other agents, the exit codes give you the same lever:
wrap risky tools in a shell guard that calls docket check and refuses on
exit 2/3. Advisory for cooperative agents, enforceable where it counts.
Why not just a sandbox?
Use one — they’re complementary layers. A sandbox bounds 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 can’t tell the
authorized email from the unauthorized one — both are legitimate traffic through the
proxy. And the failure that connects the layers is the scheduled escape: an action
that looks contained inside the session and detonates after it (a planted git hook, a
queued send). That’s a scenario family in the eval, a never in the
templates, and a rule in the spec.
What happens when no loop covers the task?
Docket 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” means depends on
where you are: the hook by default is pass-through (silent,
your normal permission flow decides), or under --strict it asks;
inside a matched loop an unlisted action is ask (the default verdict —
silence is never permission); and docket match exits 2 so
scripts and CI can branch on “unrouted.” Docket only governs what you wrote loops for;
it doesn’t seize unowned work.
How do I keep docket out of something?
It never executes anything — it only returns a verdict and appends to the record, so
there’s nothing to switch off mid-action. Engagement is opt-in per surface: don’t wire
the hook and docket only answers when explicitly asked (docket check / the
MCP tools); scope the hook’s matcher to the tools you actually want gated;
and outside a .docket project a bare hook costs nothing.
Can the record be faked?
Edits, deletions, insertions, and reordering break the hash chain at a named
entry. Tail truncation is caught by pinning the head hash
(verify --head). What a local file can’t prove is authorship — signing
the head is on the roadmap.
What goes in a loop vs. what stays out?
In: the context that changes answers, standards, hard limits. Out: secrets, tokens,
passwords — loop files are meant to be committed and shared. The
cross-tool-memory template marks storing secrets as never.
Multiple people, one repo?
Commit .docket/ and the compiled files. Everyone who clones inherits
the same warrant, and every agent working the repo is under the same rules.
What if two patterns conflict?
Order decides: never beats ask beats allow, and unlisted always asks. A target matching both an allow and an ask pattern asks.