Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions hooks/bootstrap-claude-md.sh
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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

Expand All @@ -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
5 changes: 3 additions & 2 deletions tests/hooks/test-bootstrap-claude-md.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,11 @@ feed() {
jq -n --arg c "$1" '{hook_event_name:"SessionStart",source:"startup",cwd:$c}' | bash "$HOOK" 2>/dev/null
}

# emitted <stdout> <rc> : 0 when it is a valid SessionStart nudge with non-empty context
# emitted <stdout> <rc> : 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 <stdout> <rc> : 0 when nothing was emitted and exit was clean
Expand Down