A minimal Claude Code Stop hook that keeps an autonomous agent session going until your beads queue is reconciled — i.e. no beads are still marked in_progress. Inspired by the Reeds autonomous-loop pattern + the official Ralph Wiggum "don't stop until complete" example. No framework — just a shell script and two lines of settings.local.json.
When Claude Code tries to stop a turn, this hook fires. It:
- Reads
stop_hook_activefrom stdin. If Claude has already been blocked this turn, let it stop — avoids infinite loops (this is the invariant the official hooks docs warn about). - Runs
bd list --status=in_progress— counts beads the agent has actively claimed this session. - If > 0, emits
{"decision":"block","reason":"..."}back to Claude, listing the first three remaining beads as a hint. Claude keeps going. - If 0, exits 0 silently — Claude stops normally.
- If
bdisn't installed on the host, skips gracefully — doesn't block stops in environments that don't use beads.
Net: in an autonomous session, the agent can't "declare done" with work-in-flight that it claimed. It has to either finish (via bd close <id>) or explicitly reopen (via bd update <id> --status=open) before exit.
Earlier versions gated on open P0/P1 beads. Two problems:
- Priority is project-specific. "P0" in your repo might mean "fund-safety blocker"; "P0" somewhere else might mean "nice-to-have for next sprint." The hook shouldn't hardcode either.
- Open ≠ mine. In a shared-repo beads store,
openbeads can span many authors + ages. Blocking on all of them wedges your exit on someone else's backlog.
in_progress is the one signal that means "the agent running right now claimed this and hasn't released it." That's exactly the "this session" scope.
An opt-in BD_STOP_BLOCK_ON_OPEN=1 env var additionally blocks on any open bead, for solo-repo workflows where you want the stricter gate.
Both are great. Reeds is a full loop runner; Ralph Wiggum is the official Anthropic plugin for the same pattern. This 80-line hook is the "I just want the Stop-block primitive, nothing else" answer. Use Reeds if you also want the structured loop (bd ready → bd show → implement → bd close → repeat); use this if you're already driving your own loop and just need the "don't exit yet" gate.
# 1. drop the script in your repo
mkdir -p .claude/hooks
cp check-beads-before-stop.sh .claude/hooks/
chmod +x .claude/hooks/check-beads-before-stop.sh
# 2. wire into .claude/settings.local.json
# (.claude/ is typically gitignored, so this is per-contributor)Add to .claude/settings.local.json:
{
"hooks": {
"Stop": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/check-beads-before-stop.sh"
}
]
}
]
}
}Env vars the script reads (all optional):
BD_ISSUE_PREFIX— scopebdqueries to a specific issue prefix (e.g.VA). Required when the.beads/issues.jsonlstore has mixed prefixes (shared/imported repos);bditself errors out with "mixed prefixes" in that case so the hook can't count without it.BD_STOP_BLOCK_ON_OPEN=1— additionally block on anyopenbead, not justin_progress. Strict mode; off by default.
Set them in your shell rc or in the .claude/settings.local.json hooks env field (Claude Code settings schema supports this per docs).
- Infinite-loop safety: the
stop_hook_activecheck is load-bearing. Without it, a broken bead query (saybdreturns non-zero but grep matches something unexpected) would wedge the agent forever. Keep the check. - Per-contributor:
.claude/settings.local.jsonisn't committed in most setups. If you want the hook shared across a team, commit a template to.claude/settings.json(non-local) and document the install. - "Stale" in_progress beads: if you mark
bd update X --status=in_progressfrom a prior session and forget, the hook will block you at the start of the next session. Runbd list --status=in_progresson session start to see if anything's carrying over;bd update <id> --status=opento release. - Bullet character portability: beads renders status bullets with unicode glyphs (
○ ◐ ◯ ●). The script greps for all four so it works across statuses. If your beads version uses different glyphs, extend the regex.
# Should exit 0 (queue clean)
echo '{"stop_hook_active": false}' | .claude/hooks/check-beads-before-stop.sh
# Create an in_progress bead, hook should now emit a "block" JSON
BD_ISSUE_PREFIX=VA bd create --title "smoke" --type=task --priority=4 --description "test"
BD_ISSUE_PREFIX=VA bd update VA-N --status=in_progress
BD_ISSUE_PREFIX=VA bash -c 'echo "{\"stop_hook_active\": false}" | .claude/hooks/check-beads-before-stop.sh'
# => {"decision": "block", "reason": "Don't stop yet: 1 in_progress bead(s)..."}
# stop_hook_active=true should always exit 0 (no infinite loops)
echo '{"stop_hook_active": true}' | .claude/hooks/check-beads-before-stop.sh
# Cleanup
BD_ISSUE_PREFIX=VA bd close VA-N --reason "smoke"- Claude Code Hooks reference — official.
- Beads issue tracker — what
bdis. - Beads Best Practices (Yegge) — why beads + AI.
- Reeds — full autonomous loop implementation.
- Ralph Wiggum plugin — Anthropic's official "keep going until done" example.
Public domain / CC0. Rip it, rename it, change the logic — whatever helps your loop.