The useful bits
- The approval dialog is emitted as a PermissionRequest for ExitPlanMode, not as a Stop hook.
- The hook must consume stdin and return hookSpecificOutput with hookEventName set to PermissionRequest.
- Plan markdown can be archived in the background without blocking the approval response.
if you’re using Claude Code’s plan mode, you’ve probably seen the “ready to code?” approval dialog. as I always approve Opus 4.6’s plan, no need to do this thing manually anymore. turns out you can automate that click with a single hook once you target the right event.
this also highlights a tiny docs gap tracked in anthropic’s repo: issue #11891 (“[DOCS] Missing PermissionRequest hook details in Hooks Guide and Input Reference schema”). + not well-documented and we need to do better job on documenting JSON payload details as Claude Code is not open-source, we had to do basic reverse engineering (not cool as its name, basically, just add event listener that save JSONs into a folder, and read from there, not assembly patched xD)
the key detail: it’s PermissionRequest (not Stop)
the approval dialog is a PermissionRequest for the tool ExitPlanMode.
Stop fires after a run is done. plan approval happens while Claude is waiting for user input, so Stop is the wrong place to intercept it.
what PermissionRequest sends (stdin)
here’s the (currently under-documented) shape you get on stdin when the dialog pops:
{
"session_id": "abc123-def4-5678-ghij-klmnopqrstuv",
"transcript_path": "/Users/you/.claude/projects/-Users-you-my-project/abc123.jsonl",
"cwd": "/Users/you/my-project",
"permission_mode": "plan",
"hook_event_name": "PermissionRequest",
"tool_name": "ExitPlanMode",
"tool_input": {
"allowedPrompts": [
{ "tool": "Bash", "prompt": "run tests" },
{ "tool": "Bash", "prompt": "install deps" }
],
"plan": "# Plan: Implement Feature X\n\n## Context\n\n..."
}
}
two useful bits:
tool_nametells you what is asking (here:ExitPlanMode)tool_input.plancontains the full plan markdown (which you can archive elsewhere)
minimal auto-approve hook (copy/paste)
script: ~/.claude/hooks/auto-approve-plan.sh
#!/bin/bash
cat >/dev/null # consume stdin (important on Windows/WSL)
cat <<'EOF'
{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"allow","message":"auto-approved"}}}
EOF
config (only match ExitPlanMode, not everything):
{
"hooks": {
"PermissionRequest": [
{
"matcher": "ExitPlanMode",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/auto-approve-plan.sh"
}
]
}
]
}
}
notes that save time:
- your hook should read stdin (otherwise you can hang)
- your output must be wrapped under
hookSpecificOutputwithhookEventName: "PermissionRequest"
optional: archive each plan to Craft.do
because the plan markdown is already in tool_input.plan, you can ship it to Craft.do in the background and still return the approval immediately.
the only “gotcha”: don’t pull the markdown into a shell variable and then re-embed it into JSON (multiline content will bite you). build the Craft.do payload in a single jq pass instead.
#!/bin/bash
TMPFILE="$(mktemp)"
cat > "$TMPFILE"
fire-and-forget archive (don’t block the approval response)
(
jq
--arg ts "$(date '+%Y-%m-%d %H:%M')"
--arg pageId "$CRAFT_PAGE_ID"
--arg home "$HOME"
'{
blocks: [{
type: "page",
textStyle: "card",
markdown: ("[" + (.cwd | sub($home; "~")) + "] - [" + $ts + "]"),
content: [{ type: "text", markdown: .tool_input.plan }]
}],
position: { position: "end", pageId: $pageId }
}' < "$TMPFILE"
| curl -sS -X POST "$CRAFT_API_URL/blocks" -H "Content-Type: application/json" -d @-
>/dev/null 2>&1
) &
rm -f "$TMPFILE"
approve the dialog
cat <<'EOF'
{"hookSpecificOutput":{"hookEventName":"PermissionRequest","decision":{"behavior":"allow","message":"auto-approved + archived"}}}
EOF
if you want this packaged: yigitkonur/hooks-claude-approve
i bundled the “auto-approve + (optional) Craft.do archive” flow into yigitkonur/hooks-claude-approve.
modes:
| mode | auto-approve | Craft.do archive | use case |
|---|---|---|---|
| 1 | yes | no | skip the approval dialog |
| 2 | yes | yes | skip approval and archive every plan |
| 3 | no | yes | archive plans but still click approve manually |
install:
bash <(curl -fsSL https://raw.githubusercontent.com/yigitkonur/hooks-claude-approve/main/install.sh)
links
- hooks tool: https://github.com/yigitkonur/hooks-claude-approve
- docs gap tracker: https://github.com/anthropics/claude-code/issues/11891
- official hooks guide: https://docs.anthropic.com/en/docs/claude-code/hooks
Sources & further reading
Questions & answers
Why does Stop not work for plan approval automation?
Because the model is paused waiting for user approval before the run stops. The approval prompt happens during PermissionRequest, not after execution finishes.
What is the minimum hook behavior needed?
Read stdin, match ExitPlanMode, and return an allow decision wrapped in hookSpecificOutput for the PermissionRequest event.