feat(codex): re-inject the bootstrap after context compaction via SessionStart hook

Codex 0.145 re-fires SessionStart with source:"compact" after every
context compaction, and injects hook stdout into the live model context
(both verified with sentinel probes on codex-cli 0.145.0). Compaction
replaces the transcript with a summary that sheds the using-superpowers
bootstrap and the active skill's instructions; in instrumented SDD runs
the first post-compaction dispatch that lacked a freshly printed
reminder reverted to harness defaults (fork_turns=all, inherited
frontier model) and the drift then self-perpetuated. Claude Code never
exhibits this because its SessionStart matcher (startup|clear|compact)
re-injects the bootstrap at the same moment — this hook restores that
parity on Codex.

On source:"startup" the hook emits nothing: the native plugin path owns
session-start injection, and duplicating it would recreate the
redundancy that led to the original session-start-codex removal. Output
is plain text (Codex consumes raw stdout, unlike the JSON envelopes
hooks/session-start emits for other harnesses), and every failure path
is fail-open: bad stdin, missing skill file, or any error yields empty
output and exit 0 so a hook problem can never break a session.

Ships with hooks-codex.json.example for the user-level ~/.codex/hooks.json
merge and tests covering source filtering, whitespace-tolerant matching,
decoy fields, and fail-open behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Drew Ritter
2026-07-23 12:21:49 -07:00
parent c686bb947a
commit 1744d69413
3 changed files with 181 additions and 0 deletions

View File

@@ -0,0 +1,110 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
HOOK_UNDER_TEST="$REPO_ROOT/hooks/session-start-codex"
EXAMPLE_UNDER_TEST="$REPO_ROOT/hooks/hooks-codex.json.example"
FAILURES=0
pass() {
echo " [PASS] $1"
}
fail() {
echo " [FAIL] $1"
FAILURES=$((FAILURES + 1))
}
# run_hook <stdin-payload> — echoes hook stdout; fails the calling test on
# non-zero exit. env -i mirrors the codex hook executor's clean environment.
run_hook() {
printf '%s' "$1" | env -i PATH="${PATH:-}" bash "$HOOK_UNDER_TEST"
}
echo "Codex SessionStart hook tests"
startup_payload='{"session_id":"s","hook_event_name":"SessionStart","model":"gpt-5.6-terra","source":"startup"}'
if output="$(run_hook "$startup_payload")" && [ -z "$output" ]; then
pass "source=startup emits nothing and exits 0"
else
fail "source=startup emits nothing and exits 0"
printf '%s\n' "$output" | head -3 | sed 's/^/ /'
fi
compact_payload='{"session_id":"s","hook_event_name":"SessionStart","model":"gpt-5.6-terra","source":"compact"}'
if output="$(run_hook "$compact_payload")"; then
ok=1
for needle in \
"<EXTREMELY_IMPORTANT>" \
"You have superpowers." \
"name: using-superpowers" \
"<CONTEXT_RESTORED>" \
"subagent-driven-development/SKILL.md" \
"references/codex-tools.md"; do
if [[ "$output" != *"$needle"* ]]; then
ok=0
echo " missing: $needle"
fi
done
if [ "$ok" -eq 1 ]; then
pass "source=compact emits bootstrap plus re-read addendum"
else
fail "source=compact emits bootstrap plus re-read addendum"
fi
else
fail "source=compact emits bootstrap plus re-read addendum (hook exited non-zero)"
fi
# Whitespace-tolerant source matching (serializers vary).
spaced_payload='{"hook_event_name":"SessionStart", "source" : "compact"}'
if output="$(run_hook "$spaced_payload")" && [[ "$output" == *"<CONTEXT_RESTORED>"* ]]; then
pass "whitespace around the source key still triggers injection"
else
fail "whitespace around the source key still triggers injection"
fi
if output="$(printf '' | env -i PATH="${PATH:-}" bash "$HOOK_UNDER_TEST")" && [ -z "$output" ]; then
pass "empty stdin fails open to no output, exit 0"
else
fail "empty stdin fails open to no output, exit 0"
fi
if output="$(run_hook 'not json at all {{{')" && [ -z "$output" ]; then
pass "garbage stdin fails open to no output, exit 0"
else
fail "garbage stdin fails open to no output, exit 0"
fi
# A compact mention inside some other field must not trigger injection.
decoy_payload='{"hook_event_name":"SessionStart","source":"startup","cwd":"/tmp/compact"}'
if output="$(run_hook "$decoy_payload")" && [ -z "$output" ]; then
pass "compact appearing outside the source field does not trigger"
else
fail "compact appearing outside the source field does not trigger"
fi
if node -e '
const example = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"));
const entry = example.hooks.SessionStart[0].hooks[0];
if (entry.type !== "command") {
console.error(`example hook type is ${JSON.stringify(entry.type)}, expected "command"`);
process.exit(1);
}
if (!/session-start-codex"$/.test(entry.command)) {
console.error(`unexpected example command shape: ${entry.command}`);
process.exit(1);
}
' "$EXAMPLE_UNDER_TEST"; then
pass "hooks-codex.json.example parses and invokes session-start-codex"
else
fail "hooks-codex.json.example parses and invokes session-start-codex"
fi
if [[ "$FAILURES" -gt 0 ]]; then
echo "STATUS: FAILED ($FAILURES failure(s))"
exit 1
fi
echo "STATUS: PASSED"