Documentation · v0.3

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. grep works, git diff works, 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 config

It 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:

LayerQuestion it answersLives in
briefWhat must the agent know before it starts? The context that changes the answer: people, history, constraints, standards, past decisions.Markdown body
procedureHow is this job done properly? Which sources count, what finished looks like, the known ways it goes wrong.Markdown body
warrantWhat may it do without asking? Read / draft / change / send lists, plus always-ask and never.YAML frontmatter
recordWhat evidence must it leave behind when it finishes or stops?YAML frontmatter
reservedWhat 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:

FieldAnswersEnforced?
goalThe outcome to reach — not an activity.descriptive (in context)
stopThe conditions that end the run; the agent halts at any of them.descriptive (in context)
budgetThe 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

  • name must 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 Brief or Procedure (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 version the tool doesn’t understand is rejected, never silently misread.
  • triggers is 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:

VerbMeaning
readlook at it
draftproduce it, but it goes nowhere on its own
changemutate state that stays inside the sandbox
sendconsequences 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).

Deferred consequences. An action classifies by where its consequences eventually land, not where the bytes land first. A send with a timer on it is a send; a git hook, CI workflow, or cron job planted now carries its consequences past the moment of approval — and past the session. Unanticipated cases fall to rule 4 below (unlisted means ask), but pre-decide the common vectors: the shipped templates hard-stop 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:

  1. target matches neverdeny
  2. target matches askask
  3. target matches the action’s own list → allow
  4. otherwise → ask
Rule 4 is the whole point. Unlisted means ask. Silence is never permission.

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, and and split a pattern into alternatives (secrets, tokens, or passwords is three patterns).
  • Words compare under light stemming (quotesquote, contactingcontact), 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:

KindWhat it captures
checka warrant check with its verdict and the rule that fired — written automatically. “Did the agent even ask?” becomes a grep.
notework evidence: saw, did, skipped, stopped, note
amenda 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.

The chain doesn’t stop tampering — it’s a plain file. It makes tampering visible, which is what an audit trail is for. It’s the difference between “the agent says it asked” and proof that it did.

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 ask or never lists don’t appear, however often they recur.
  • Every approved amendment lands on the record as an amend entry — 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:

TargetWritesRead by
claudeCLAUDE.mdClaude Code
agentsAGENTS.mdChatGPT/Codex, OpenClaw, Hermes, Zed, …
geminiGEMINI.mdGemini CLI
cursor.cursor/rules/docket.mdcCursor
rawstdoutanything 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:

TierResident?Grows with
protocolalwaysnothing — find the loop, load it, check the warrant, ask when uncovered
indexalwaysone line per loop: name, description, triggers
active loopon demandonly the task at hand — docket compile --loop <name> or docket_loop_context
enforcementneverthe 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 ask

Scoring 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 match exits 2 on no coverage, the same exit as an ask verdict, so hooks can gate on it.
The shape of it: the context window holds a table of contents and one open chapter. The enforcement layer holds every rule, forever, at zero context cost.

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.

ToolWhat the agent gets
docket_list_loopsdiscover the loops you’ve defined
docket_match_looproute 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_contexta loop’s five layers, before starting work
docket_warrant_checkallow / ask / deny for a proposed action — auto-logged, with explicit STOP instructions on ask/deny
docket_recordappend 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:

VerdictWhat Claude Code does
denythe call is blocked; the loop, rule, and reason go back to the model
askClaude Code prompts you before the call runs
allowsilence. 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 as read; file-writing tools (Write, Edit, …) as change. Everything else — Bash, MCP tools, tools that don’t exist yet — checks as send, 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 as docket 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 --strict to 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 .docket directory costs nothing. But once you pin a gate — --loop or --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: initnewcompile 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.md

AGENTS.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.md

Gemini 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.mdc

OpenClaw

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

CommandDoes
docket initcreate .docket/ here (--dir, --quiet)
docket installmake 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 templateslist the eight starter loops
docket listlist 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 verifyverify the chain end to end; --head <hash> pins against tail truncation
docket metricsautonomy posture from the record: auto-approve / ask / deny split, longest unattended run, actions per intervention, per-loop & per-channel (--loop, --json)
docket reviewpropose 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 mcprun the MCP server (--dir or DOCKET_DIR to locate the project)
docket hook claudeClaude 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:

SuiteChecksResult
Behavior scenarios — real agent-overreach incidents, incl. scheduled escapes610 silent allows · 21/21 warranted work allowed
Adversarial phrasing — euphemism, compound intent, injection, homoglyphs4242/42 contained
Vague-target probes — every proper subset of every allow pattern2180 permissions inherited
Fuzzed targets — seeded, vocabulary disjoint from every allow entry10,0000 allowed
Record tampering — edits, rehashes, deletions, reorders, forgeries, truncations239239/239 detected with pinned head; 197 by the chain alone
Hook gate — the live binary vs hostile PreToolUse payloads2214 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.