From b63149558f8543288ebee93c2b106627da92b5dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=BChh?= Date: Sat, 8 Aug 2026 09:53:54 -0400 Subject: [PATCH] fix(codex): wrap SessionStart hook output in hookSpecificOutput envelope (#1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit plugin/codex/scripts/session-start.sh and post-compaction.sh emitted the Memory Protocol text as raw markdown via `cat <<'PROTOCOL'`, not codex's expected {"hookSpecificOutput":{"hookEventName":...,"additionalContext": ...}} JSON envelope. Codex's SessionStart parser rejects raw text as a hook failure — non-fatal (the turn still completes), but every run reports 'hook: SessionStart Failed' for these two hooks. Both register under the SessionStart event (hooks.json: "startup|resume|clear" for session-start.sh, "compact" for post-compaction.sh), so hookEventName is "SessionStart" for both. The claude-code/ variant is untouched — Claude Code's SessionStart contract is tolerant of raw text, so it isn't affected. Content is now written to temp file(s) and read into jq via --rawfile, never captured with $(cat <<'EOF' ...). macOS ships bash 3.2 as /bin/bash, and bash 3.2 mis-parses a heredoc nested inside a command substitution once the heredoc body contains an apostrophe (e.g. "user's") — it throws 'unexpected EOF while looking for matching `)'' at script-load time. Both protocol blocks contain that apostrophe, so the naive $(heredoc) fix broke immediately under stock macOS bash; routing through a file sidesteps the bug. Verified: `echo '{"session_id":"x","cwd":"","hook_event_name": "SessionStart","source":"startup"}' | bash plugin/codex/scripts/session-start.sh` (and post-compaction.sh with source:"compact") now produce valid JSON with hookSpecificOutput.hookEventName == "SessionStart", exit 0. bash -n and shellcheck -S warning clean on both files. --- plugin/codex/scripts/post-compaction.sh | 36 +++++++++++++++++-------- plugin/codex/scripts/session-start.sh | 24 ++++++++++++----- 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/plugin/codex/scripts/post-compaction.sh b/plugin/codex/scripts/post-compaction.sh index 363d6be6..901b417d 100755 --- a/plugin/codex/scripts/post-compaction.sh +++ b/plugin/codex/scripts/post-compaction.sh @@ -31,8 +31,24 @@ fi ENCODED_PROJECT=$(printf '%s' "$PROJECT" | jq -sRr @uri) CONTEXT=$(curl -sf "${ENGRAM_URL}/context?project=${ENCODED_PROJECT}" --max-time 3 2>/dev/null | jq -r '.context // empty') -# Inject Memory Protocol + compaction instruction + context -cat <<'PROTOCOL' +# Build Memory Protocol + compaction instruction + context, then emit it as +# Codex's hookSpecificOutput JSON envelope. Codex's SessionStart parser (this +# hook registers under the SessionStart "compact" matcher — see hooks.json) +# rejects raw stdout text as a hook failure (non-fatal, but reports +# "hook: SessionStart Failed" every run — see codex-review SKILL.md "Failure +# modes", 2026-08-08 silent-death-after-hooks). Claude Code's SessionStart +# contract stays tolerant of raw text, so this is a codex/-only change; +# plugin/claude-code/scripts/post-compaction.sh is untouched. +# +# Each piece is written to a FILE, never captured via `$(cat <<'EOF' ...)` — +# macOS ships bash 3.2 as /bin/bash, which mis-parses a heredoc nested inside +# a command substitution once the heredoc body contains an apostrophe (e.g. +# "user's"), throwing "unexpected EOF while looking for matching `)'" at +# script-load time. Writing to files sidesteps the parser bug entirely. +TMPD=$(mktemp -d) +trap 'rm -rf "$TMPD"' EXIT + +cat <<'PROTOCOL' > "$TMPD/head.txt" ## Engram Persistent Memory — ACTIVE PROTOCOL You have engram memory tools. This protocol is MANDATORY and ALWAYS ACTIVE. @@ -68,11 +84,10 @@ Call `mem_session_summary` with: Goal, Discoveries, Accomplished, Next Steps, Re CRITICAL INSTRUCTION POST-COMPACTION — follow these steps IN ORDER: PROTOCOL -printf "\n1. FIRST: Call mem_session_summary with the content of the compacted summary above. Use project: '%s'.\n" "$PROJECT" -printf " This preserves what was accomplished before compaction.\n\n" -printf "2. THEN: Call mem_context with project: '%s' to recover recent session history and observations.\n" "$PROJECT" -printf " Read the returned context carefully — it tells you what was being worked on.\n\n" -cat <<'PROTOCOL' +printf "1. FIRST: Call mem_session_summary with the content of the compacted summary above. Use project: '%s'.\n This preserves what was accomplished before compaction.\n\n2. THEN: Call mem_context with project: '%s' to recover recent session history and observations.\n Read the returned context carefully — it tells you what was being worked on." \ + "$PROJECT" "$PROJECT" > "$TMPD/steps.txt" + +cat <<'PROTOCOL' > "$TMPD/tail.txt" 3. If you need more detail on a specific topic, call mem_search with relevant keywords. 4. Only THEN continue working on what the user asked. @@ -80,9 +95,8 @@ cat <<'PROTOCOL' All 4 steps are MANDATORY. Without them, you lose context and start blind. PROTOCOL -# Inject memory context if available -if [ -n "$CONTEXT" ]; then - printf "\n%s\n" "$CONTEXT" -fi +jq -n --rawfile head "$TMPD/head.txt" --rawfile steps "$TMPD/steps.txt" --rawfile tail "$TMPD/tail.txt" --arg ctx "$CONTEXT" \ + '{hookSpecificOutput: {hookEventName: "SessionStart", + additionalContext: ($head + "\n\n" + $steps + "\n\n" + $tail + (if $ctx != "" then "\n\n" + $ctx else "" end))}}' exit 0 diff --git a/plugin/codex/scripts/session-start.sh b/plugin/codex/scripts/session-start.sh index 19a85972..22487de9 100755 --- a/plugin/codex/scripts/session-start.sh +++ b/plugin/codex/scripts/session-start.sh @@ -137,8 +137,21 @@ fi ENCODED_PROJECT=$(printf '%s' "$PROJECT" | jq -sRr @uri) CONTEXT=$(curl -sf "${ENGRAM_URL}/context?project=${ENCODED_PROJECT}" --max-time 3 2>/dev/null | jq -r '.context // empty') -# Inject Memory Protocol + context — stdout is returned to Codex as additionalContext -cat <<'PROTOCOL' +# Build Memory Protocol + context, then emit it as Codex's hookSpecificOutput +# JSON envelope. Codex's SessionStart parser rejects raw stdout text as a hook +# failure (non-fatal, but reports "hook: SessionStart Failed" every run — see +# codex-review SKILL.md "Failure modes", 2026-08-08 silent-death-after-hooks). +# Claude Code's SessionStart contract stays tolerant of raw text, so this is a +# codex/-only change; plugin/claude-code/scripts/session-start.sh is untouched. +# +# The protocol text is written to a FILE, never captured via `$(cat <<'EOF' ...)` +# — macOS ships bash 3.2 as /bin/bash, which mis-parses a heredoc nested inside +# a command substitution once the heredoc body contains an apostrophe (e.g. +# "user's"), throwing "unexpected EOF while looking for matching `)'" at +# script-load time. Writing to a file sidesteps the parser bug entirely. +PROTOCOL_FILE=$(mktemp) +trap 'rm -f "$PROTOCOL_FILE"' EXIT +cat <<'PROTOCOL' > "$PROTOCOL_FILE" ## Engram Persistent Memory — ACTIVE PROTOCOL You have engram memory tools. This protocol is MANDATORY and ALWAYS ACTIVE. @@ -174,9 +187,8 @@ Call `mem_save` IMMEDIATELY after ANY of these: Call `mem_session_summary` with: Goal, Discoveries, Accomplished, Next Steps, Relevant Files. PROTOCOL -# Inject memory context if available -if [ -n "$CONTEXT" ]; then - printf "\n%s\n" "$CONTEXT" -fi +jq -n --rawfile protocol "$PROTOCOL_FILE" --arg ctx "$CONTEXT" \ + '{hookSpecificOutput: {hookEventName: "SessionStart", + additionalContext: ($protocol + (if $ctx != "" then "\n\n" + $ctx else "" end))}}' exit 0