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
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
{
"name": "autodev",
"description": "Autonomous development workflow skills for coding agents",
"version": "6.1.2",
"version": "6.1.3",
"source": "./",
"author": {
"name": "Jon Langevin",
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "autodev",
"description": "Autonomous development workflow skills for coding agents: design, review, planning, execution, monitoring, and retrospectives",
"version": "6.1.2",
"version": "6.1.3",
"author": {
"name": "Jon Langevin",
"email": "jon@gocodealone.com"
Expand Down
2 changes: 1 addition & 1 deletion .cursor-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "autodev",
"displayName": "Autonomous Dev Kit",
"description": "Autonomous development workflow skills for coding agents",
"version": "6.1.2",
"version": "6.1.3",
"author": {
"name": "Jon Langevin",
"email": "jon@gocodealone.com"
Expand Down
8 changes: 8 additions & 0 deletions RELEASE-NOTES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Autonomous Dev Kit Release Notes

## v6.1.3 — 2026-05-27

PreToolUse / SubagentStop block contract fix for Codex compatibility.

- `hooks/pre-tool-scope-guard` and `hooks/subagent-scope-guard`: the `block()` helper was emitting `{"decision":"block","reason":"..."}` on stdout and then `exit 2`. Both Claude Code and Codex ignore stdout JSON when a hook exits with code 2 — they require the reason on stderr. Codex enforces this strictly and surfaced the error: `PreToolUse hook exited with code 2 but did not write a blocking reason to stderr`. Claude Code silently dropped the reason. Fixed by switching to `exit 0` with stdout JSON (the documented decision-control path on both hosts) and mirroring the reason to stderr for any host that captures stderr regardless of exit code. Same pattern already used by `hooks/completion-claim-guard`.
- Switched `jq -n` → `jq -nc` in both hooks so the emitted JSON is compact (matches the format hosts and grep-based tests expect; trims a few bytes).
- Added two regression tests in `tests/hook-contracts.sh` that assert blocks exit 0, emit stdout JSON, AND mirror the reason to stderr.

## v6.1.2 — 2026-05-27

SessionStart hook payload bloat fix.
Expand Down
18 changes: 14 additions & 4 deletions hooks/pre-tool-scope-guard
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,22 @@ transcript_path=$(printf '%s' "$hook_input" | jq -r '.transcript_path // empty'
session_key=""
[ -n "$transcript_path" ] && session_key=$(basename "$transcript_path")

# Output a block decision and exit 2 (the exit code Claude Code uses for blocks).
# Output a block decision via JSON on stdout, then exit 0.
# Both Claude Code and Codex parse `{"decision":"block","reason":"..."}` on
# stdout when the hook exits 0. We deliberately do NOT use `exit 2`: both
# hosts ignore stdout entirely on exit 2 and require the reason on stderr,
# so emitting JSON + exit 2 would surface "exited with code 2 but did not
# write a blocking reason to stderr" on Codex and silently drop the reason
# on Claude Code. Mirror the pattern used in hooks/completion-claim-guard.
# Also mirror the reason to stderr so hosts/logs that capture stderr still
# see the human-readable text.
block() {
local reason="$1"
printf '{"decision":"block","reason":%s}\n' \
"$(printf '%s' "$reason" | jq -Rs .)"
exit 2
if command -v jq >/dev/null 2>&1; then
jq -nc --arg reason "$reason" '{decision:"block",reason:$reason}'
fi
printf '%s\n' "$reason" >&2
exit 0
}

# Recognized helper script names that update session-lock state. Pattern-matched
Expand Down
8 changes: 5 additions & 3 deletions hooks/subagent-scope-guard
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,11 @@ stop_hook_active=$(printf '%s' "$hook_input" | jq -r '.stop_hook_active // false

block() {
local reason="$1"
printf '{"decision":"block","reason":%s}\n' \
"$(printf '%s' "$reason" | jq -Rs .)"
exit 2
if command -v jq >/dev/null 2>&1; then
jq -nc --arg reason "$reason" '{decision:"block",reason:$reason}'
fi
printf '%s\n' "$reason" >&2
exit 0
}

# ── Detect uncommitted or recently committed protected-file changes ───────────
Expand Down
75 changes: 75 additions & 0 deletions tests/hook-contracts.sh
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,79 @@ test_session_start_json() {
assert_hook_context_json "session-start" "SessionStart" "$output"
}

test_pre_tool_scope_guard_block_exits_zero_with_stderr_reason() {
# When pre-tool-scope-guard blocks a Bash command, it must:
# (1) exit 0 -- both Claude Code and Codex ignore stdout JSON on exit 2
# (2) emit {"decision":"block","reason":"..."} on stdout (Claude Code path)
# (3) mirror the reason on stderr (Codex path / any host that reads stderr)
# Regression for Codex error: "PreToolUse hook exited with code 2 but did
# not write a blocking reason to stderr."
local tmp stdout_file stderr_file status
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' RETURN
stdout_file="$tmp/out"
stderr_file="$tmp/err"
# force-push trigger: always blocked, no setup required
set +e
printf '%s' '{"tool_name":"Bash","tool_input":{"command":"git push --force origin main"},"cwd":"'"$tmp"'"}' \
| hooks/pre-tool-scope-guard >"$stdout_file" 2>"$stderr_file"
status=$?
set -e
if [ "$status" != "0" ]; then
fail "pre-tool-scope-guard: block must exit 0, got ${status}. stderr: $(cat "$stderr_file")"
return
fi
if ! grep -q '"decision":"block"' "$stdout_file"; then
fail "pre-tool-scope-guard: block must emit JSON on stdout, got: $(cat "$stdout_file")"
return
fi
if ! grep -qi 'force push' "$stderr_file"; then
fail "pre-tool-scope-guard: block must mirror reason to stderr, got: $(cat "$stderr_file")"
return
fi
pass "pre-tool-scope-guard: block emits exit 0 + stdout JSON + stderr text (Codex compat)"
}

test_subagent_scope_guard_block_exits_zero_with_stderr_reason() {
# Same contract for the SubagentStop hook.
local tmp transcript stdout_file stderr_file status
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' RETURN
transcript="$tmp/session.jsonl"
touch "$transcript"
mkdir -p "$tmp/docs/plans" "$tmp/.claude/autodev-state" "$tmp/tests"
cp "$REPO_ROOT/tests/plan-scope-check.sh" "$tmp/tests/plan-scope-check.sh"
chmod +x "$tmp/tests/plan-scope-check.sh"
emit_locked_fixture "$tmp/docs/plans/active.md" "active"
jq -nc --arg s "session.jsonl" --arg pl "docs/plans/active.md" \
'{ev:"session-lock",session:$s,pl:$pl}' \
> "$tmp/.claude/autodev-state/session-locks.jsonl"
# Force drift so verify-lock fails and block() fires.
awk '/^\*\*Tasks:\*\* 1/ {print; print "**Drift:** yes"; next} {print}' \
"$tmp/docs/plans/active.md" > "$tmp/docs/plans/active.md.tmp" \
&& mv "$tmp/docs/plans/active.md.tmp" "$tmp/docs/plans/active.md"
stdout_file="$tmp/out"
stderr_file="$tmp/err"
set +e
printf '%s' '{"cwd":"'"$tmp"'","transcript_path":"'"$transcript"'","stop_hook_active":false}' \
| hooks/subagent-scope-guard >"$stdout_file" 2>"$stderr_file"
status=$?
set -e
if [ "$status" != "0" ]; then
fail "subagent-scope-guard: block must exit 0, got ${status}. stderr: $(cat "$stderr_file")"
return
fi
if ! grep -q '"decision":"block"' "$stdout_file"; then
fail "subagent-scope-guard: block must emit JSON on stdout, got: $(cat "$stdout_file")"
return
fi
if ! grep -qi 'manifest' "$stderr_file"; then
fail "subagent-scope-guard: block must mirror reason to stderr, got: $(cat "$stderr_file")"
return
fi
pass "subagent-scope-guard: block emits exit 0 + stdout JSON + stderr text (Codex compat)"
}

test_wrapper_suppresses_unavailable_c_utf8_locale_noise() {
local tmp stdout_file stderr_file stderr_text stdout_text
tmp="$(mktemp -d)"
Expand Down Expand Up @@ -1404,6 +1477,8 @@ test_pretool_allows_locked_plan_text_edit
test_subagent_allows_non_manifest_plan_backport
test_subagent_scope_guard_ignores_unattributed_workspace_lock
test_subagent_scope_guard_blocks_attributed_drift
test_pre_tool_scope_guard_block_exits_zero_with_stderr_reason
test_subagent_scope_guard_block_exits_zero_with_stderr_reason
test_scope_lock_claim_writes_session_attribution
test_scope_lock_claim_writes_are_idempotent
test_scope_lock_claim_rejects_unlocked_plan
Expand Down
Loading