aboutsummaryrefslogtreecommitdiff
path: root/hooks
diff options
context:
space:
mode:
Diffstat (limited to 'hooks')
-rw-r--r--hooks/README.md1
-rwxr-xr-xhooks/inbox-boundary-check.sh52
-rw-r--r--hooks/settings-snippet.json1
3 files changed, 54 insertions, 0 deletions
diff --git a/hooks/README.md b/hooks/README.md
index 71b3613..e83469e 100644
--- a/hooks/README.md
+++ b/hooks/README.md
@@ -10,6 +10,7 @@ Machine-wide Claude Code hooks that install into `~/.claude/hooks/` and apply to
| `git-commit-confirm.py` | `PreToolUse(Bash)` | Silent-unless-suspicious gate on `git commit`. Only prompts when the message contains AI-attribution patterns, the message can't be parsed (editor would open), no files are staged, or the git author is unusable. Clean commits pass through without a modal. Parses both HEREDOC and `-m`/`--message` forms. |
| `gh-pr-create-confirm.py` | `PreToolUse(Bash)` | Gates `gh pr create` behind a confirmation modal showing title, base←head, reviewers, labels, assignees, milestone, draft flag, and body (HEREDOC or quoted). |
| `destructive-bash-confirm.py` | `PreToolUse(Bash)` | Gates destructive commands (`git push --force`, `git reset --hard`, `git clean -f`, `git branch -D`, `rm -rf`) with a modal showing the command, local context (branch, uncommitted file counts, targeted paths), and a warning banner. Elevates severity when force-pushing protected branches or targeting root/home/wildcard paths. |
+| `inbox-boundary-check.sh` | `Stop` | Soft-nudges the agent to process pending `inbox/` handoffs before yielding. Blocks the stop once (injects a reason with the pending count) when `inbox-status -q` reports pending items; steps aside on the harness re-entry (`stop_hook_active`) so a mid-task pause or an unprocessable item never wedges. No-ops in any project without an `inbox/` or without `inbox-status`. |
Shared library (not a hook): `_common.py` — `read_payload()`, `respond_ask()`, `scan_attribution()`. Installed as a sibling symlink so the two Python hooks can `from _common import …` at runtime.
diff --git a/hooks/inbox-boundary-check.sh b/hooks/inbox-boundary-check.sh
new file mode 100755
index 0000000..916c000
--- /dev/null
+++ b/hooks/inbox-boundary-check.sh
@@ -0,0 +1,52 @@
+#!/usr/bin/env bash
+# Stop hook: soft-nudge the agent to process pending inbox/ handoffs before it
+# yields control back to Craig.
+#
+# The "check inbox/ at every task boundary" rule (protocols.org, Inbox
+# Monitoring Cadence) is otherwise prose-only, so it holds only as well as the
+# agent remembers it. A Stop is the agent finishing a turn and about to yield,
+# which is the harness's closest event to the rule's own trigger ("after
+# finishing a unit of work, before reporting back"). When handoffs are pending
+# this hook blocks the yield once and injects a reason, so the agent processes
+# them instead of returning with items unseen.
+#
+# Soft-nudge, not hard-block: on the harness re-entry (stop_hook_active: true)
+# the hook steps aside and lets the turn end. An item the agent genuinely can't
+# process, or a mid-task pause to ask Craig a question (also a Stop), never
+# wedges the session.
+#
+# Self-skips everywhere it doesn't apply: no inbox/ dir, no inbox-status, or a
+# clean inbox each exit 0 silently. One hook, every project, no config.
+#
+# Wire in ~/.claude/settings.json (see hooks/settings-snippet.json), Stop array.
+set -u
+
+payload="$(cat)"
+
+# Re-entry after our own nudge: don't nudge twice, let the turn end.
+active="$(printf '%s' "$payload" | jq -r '.stop_hook_active // false' 2>/dev/null)"
+[ "$active" = "true" ] && exit 0
+
+cwd="$(printf '%s' "$payload" | jq -r '.cwd // empty' 2>/dev/null)"
+[ -z "$cwd" ] && cwd="$PWD"
+cd "$cwd" 2>/dev/null || exit 0
+
+# Nothing to enforce without an inbox to watch.
+[ -d inbox ] || exit 0
+
+# Prefer the project-local inbox-status; fall back to one on PATH. Absent both,
+# degrade to a no-op rather than block on a check we can't run.
+status_bin=".ai/scripts/inbox-status"
+[ -x "$status_bin" ] || status_bin="$(command -v inbox-status 2>/dev/null)" || exit 0
+[ -n "$status_bin" ] || exit 0
+
+# inbox-status -q: exit 1 = pending, 0 = clean, 2 = no inbox. Only 1 nudges.
+out="$("$status_bin" -q 2>/dev/null)"
+[ $? -eq 1 ] || exit 0
+
+count="$(printf '%s' "$out" | grep -oE '[0-9]+' | head -1)"
+[ -n "$count" ] || count="Some"
+
+reason="$count pending inbox handoff(s) in $(basename "$cwd"). Process per inbox.org before yielding."
+printf '{"decision":"block","reason":%s}\n' "$(printf '%s' "$reason" | jq -Rs .)"
+exit 0
diff --git a/hooks/settings-snippet.json b/hooks/settings-snippet.json
index 0f0e784..26cbc18 100644
--- a/hooks/settings-snippet.json
+++ b/hooks/settings-snippet.json
@@ -34,6 +34,7 @@
"Stop": [
{
"hooks": [
+ { "type": "command", "command": "~/.claude/hooks/inbox-boundary-check.sh" },
{ "type": "command", "command": "~/.claude/hooks/ai-wrap-teardown.sh" }
]
}