From d89637e933841ca93b0ba6370712a8735dde26bf Mon Sep 17 00:00:00 2001 From: Dennis Vorobyov Date: Mon, 14 Sep 2026 12:36:04 +0100 Subject: [PATCH] fix(hooks): bootstrap hook injects context via plain stdout The SessionStart bootstrap hook emitted its nudge as a hookSpecificOutput additionalContext JSON object. Plain-text stdout is the documented way a SessionStart hook injects context Claude can see, so emit the nudge directly and drop the jq output call (jq is still used to read the stdin payload). Behavior is otherwise unchanged: silent unless a git repo lacks a root CLAUDE.md. Test asserts the plain-text nudge instead of parsing JSON. --- hooks/bootstrap-claude-md.sh | 12 +++++++----- tests/hooks/test-bootstrap-claude-md.sh | 5 +++-- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/hooks/bootstrap-claude-md.sh b/hooks/bootstrap-claude-md.sh index ba56387..a08528e 100755 --- a/hooks/bootstrap-claude-md.sh +++ b/hooks/bootstrap-claude-md.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash # SessionStart hook -- offer to bootstrap a project CLAUDE.md when the repo has none. # -# Emits a one-line nudge (as additionalContext) telling Claude to act on the -# project-bootstrap policy (CLAUDE.md section 4C). Stays completely silent unless +# Emits a one-line nudge on stdout (the documented SessionStart context idiom) telling +# Claude to act on the project-bootstrap policy (CLAUDE.md section 4C). Silent unless # ALL of these hold: # - the session's directory is inside a git work tree, # - the work-tree root is not the home directory (never bootstrap a dotfiles-in-$HOME @@ -14,8 +14,8 @@ # the tool's own target" rule (STRUCTURE.md). # # Contract: reads the SessionStart JSON payload on stdin (uses .cwd), prints either -# nothing or one hookSpecificOutput JSON object, and exits 0. Requires jq and git, -# already hard dependencies of this config's hooks. +# nothing or one line of context to stdout, and exits 0. Requires jq (to read the +# payload) and git, already hard dependencies of this config's hooks. set -u @@ -42,5 +42,7 @@ canon=$(cd "$home" 2>/dev/null && pwd -P 2>/dev/null) msg="This repository has no CLAUDE.md at its root. Follow the project-bootstrap policy (CLAUDE.md section 4C): offer to create one before doing other work, and create it only if the user agrees. On yes, if the repo is essentially empty use the new-repo skill; otherwise create ./CLAUDE.md from the standard template at ~/.claude/templates/CLAUDE.project.md, fill the section 19 fields you can confidently detect now (stack, quality-gate commands), and leave the rest as TODO to complete as specs appear. Never overwrite an existing CLAUDE.md." -jq -cn --arg c "$msg" '{hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:$c}}' +# Plain-text stdout is the documented way a SessionStart hook injects context Claude +# can see; print the nudge and exit clean. +printf '%s\n' "$msg" exit 0 diff --git a/tests/hooks/test-bootstrap-claude-md.sh b/tests/hooks/test-bootstrap-claude-md.sh index 101350a..da17ffc 100755 --- a/tests/hooks/test-bootstrap-claude-md.sh +++ b/tests/hooks/test-bootstrap-claude-md.sh @@ -21,10 +21,11 @@ feed() { jq -n --arg c "$1" '{hook_event_name:"SessionStart",source:"startup",cwd:$c}' | bash "$HOOK" 2>/dev/null } -# emitted : 0 when it is a valid SessionStart nudge with non-empty context +# emitted : 0 when a non-empty nudge was printed to stdout (the documented +# SessionStart context idiom is plain text) and exit was clean emitted() { [ "$2" -eq 0 ] || return 1 - printf '%s' "$1" | jq -e '.hookSpecificOutput | .hookEventName == "SessionStart" and (.additionalContext | length > 0)' >/dev/null 2>&1 + [ -n "$1" ] && printf '%s' "$1" | grep -q "no CLAUDE.md at its root" } # silent : 0 when nothing was emitted and exit was clean