Approval Tokens (ccchain approve)
The approval-token flow lets a human owner unblock a specific command that ccchain denied through the ask → deny degrade path — turning "the agent got blocked in auto mode" into an out-of-band conversation instead of a dead end.
Threat Model (why this design)
The hint text that ccchain returns as permissionDecisionReason reaches the agent's context. A token embedded in that hint would let the agent self-approve. The flow therefore:
- Never puts a token in the hint — only the procedure (
ccchain approve --last) is written, and only the owner can act on it in a shell outside Claude Code - Records pending requests server-side (
~/.claude/ccchain/pending.jsonl) so the owner sees exactly what the agent asked for - Fences off
ccchain approvefrom the agent's Bash tool via the sentinel preset (allow ccchain / args: ^approve\b: deny). For defense in depth, also addBash(ccchain approve*)tosettings.jsonpermissions.deny
Flow
┌───────────────────────────┐
│ agent runs a Bash command │
└──────────────┬────────────┘
│
▼
┌────────────────┐ interactive mode ┌────────────────┐
│ evaluate → ask │ ──────────────────────────► │ ask dialog │
└───────┬────────┘ │ (human clicks) │
│ non-interactive └────────────────┘
│ (auto / dontAsk / …)
▼
┌──────────────────────────────┐
│ ask degrades to deny + hint │
│ + record pending.jsonl entry │
└──────────────┬───────────────┘
│
▼
agent sees: "run `ccchain approve --last`"
│
▼
owner (shell outside Claude Code):
$ ccchain approve --last
→ writes approved.jsonl (TTL, scope)
│
▼
agent retries the same command
│
▼
┌──────────────────────────────┐
│ hook matches approval → allow │
│ (one-shot: entry consumed) │
└──────────────────────────────┘CLI
ccchain approve --last # approve the most recent pending entry
ccchain approve --list # show pending entries (# / hash / age / command)
ccchain approve <hash-prefix> # approve by hash prefix (min 4 chars)
ccchain approve --revoke-all # mark every un-consumed approval as spentFlags:
| Flag | Description |
|---|---|
--ttl <duration> | Approval lifetime. Default 15m. Accepts Go durations: 30s, 1h, ... |
--global | Match any session / cwd. Default is session+cwd only |
-h, --help | Show usage |
Example session:
# 1. Agent tries a command; ccchain records pending
$ ccchain approve --list
# HASH AGE COMMAND
1 a1b2c3d4e5f6 3s git push origin main# 2. Owner approves in their own terminal
$ ccchain approve --last --ttl 1h
approved: a1b2c3d4e5f6
command: git push origin main
scope: session
cwd: /home/user/project
session: 01HXYZ...
ttl: 1h0m0sThe next time the agent runs the identical command from the same session
- cwd within the TTL, ccchain returns
permissionDecision: "allow"and the pending entry is consumed (one-shot).
Matching Rules
- Normalization: commands are normalized by re-emitting the parsed shell AST (
mvdan.cc/shprinter) before hashing (SHA-256). Whitespace and quoting differences (ls -lavsls -lavsls "-la") collapse to the same hash - Scope (default: session):
session_id+cwdmust match the entry recorded when the deny happened.--globalwidens this to any session and any directory - TTL (default: 15m): the approval expires TTL after
ccchain approvewas run. Expired approvals are never consumed - One-shot: consumption is atomic per hook invocation; a granted approval covers exactly one retry
Dynamic Commands are Ineligible
Commands containing $VAR, $(...), or backticks are not eligible for approval. Their expansion depends on runtime state, so the hash cannot guarantee "the same command." When ccchain sees a dynamic command in the degrade path, the deny message is appended with the reason and the human is asked to rewrite the command with literal arguments (or run it in an interactive session).
Storage
- Directory:
$CLAUDE_CONFIG_DIR/ccchain/ifCLAUDE_CONFIG_DIRis set, otherwise~/.claude/ccchain/ - Files:
pending.jsonlandapproved.jsonl(append-only) - Permissions:
0600(owner read/write only) - Locking:
O_EXCLlock file for concurrent access safety — no external dependencies required - Audit: approvals and consumptions are also written to the ccchain audit log (see
ccchain audit)
Security Notes
- Do not run
ccchain approvefrom an agent shell. The sentinel preset denies it, but only if you use the sentinel preset. As defense in depth, addBash(ccchain approve*)tosettings.json'spermissions.deny - Approvals are per-machine; there is no network path
--revoke-allis the emergency stop button — it invalidates every un-consumed approval and forces the flow to start over