From da8d24dd5a55154981a5c2441a4a17acf364e636 Mon Sep 17 00:00:00 2001 From: Kevin Turcios Date: Tue, 21 Apr 2026 05:15:05 -0500 Subject: [PATCH 1/3] =?UTF-8?q?perf:=20replace=20Node.js=20SessionStart=20?= =?UTF-8?q?hook=20with=20bash=20(106ms=20=E2=86=92=205ms)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SessionStart handler only appends two env vars (session ID and plugin data dir) to CLAUDE_ENV_FILE. Running this through Node.js pays ~100ms of V8 startup overhead on every session. Replace with an equivalent bash script that performs the same shell-escaped env var exports. SessionEnd stays in Node.js since it requires async broker teardown and process tree management. Benchmarked on macOS (Apple Silicon): Before: 106 ms ± 1.3 ms After: 5 ms ± 5.5 ms --- plugins/codex/hooks/hooks.json | 2 +- plugins/codex/scripts/session-start-env.sh | 25 ++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100755 plugins/codex/scripts/session-start-env.sh diff --git a/plugins/codex/hooks/hooks.json b/plugins/codex/hooks/hooks.json index 19e33b81..37e3e1d8 100644 --- a/plugins/codex/hooks/hooks.json +++ b/plugins/codex/hooks/hooks.json @@ -6,7 +6,7 @@ "hooks": [ { "type": "command", - "command": "node \"${CLAUDE_PLUGIN_ROOT}/scripts/session-lifecycle-hook.mjs\" SessionStart", + "command": "bash \"${CLAUDE_PLUGIN_ROOT}/scripts/session-start-env.sh\"", "timeout": 5 } ] diff --git a/plugins/codex/scripts/session-start-env.sh b/plugins/codex/scripts/session-start-env.sh new file mode 100755 index 00000000..fdbc9b5b --- /dev/null +++ b/plugins/codex/scripts/session-start-env.sh @@ -0,0 +1,25 @@ +#!/bin/bash +# SessionStart hook: export session ID and plugin data dir to CLAUDE_ENV_FILE. +# Replaces the Node.js session-lifecycle-hook.mjs for the SessionStart path +# to avoid ~100ms V8 startup overhead on every session. + +set -uo pipefail + +INPUT=$(cat) +SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty' 2>/dev/null) + +[ -z "${CLAUDE_ENV_FILE:-}" ] && exit 0 + +shell_escape() { + printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\"\\'\"'/g")" +} + +if [ -n "$SESSION_ID" ]; then + printf 'export CODEX_COMPANION_SESSION_ID=%s\n' "$(shell_escape "$SESSION_ID")" >> "$CLAUDE_ENV_FILE" +fi + +if [ -n "${CLAUDE_PLUGIN_DATA:-}" ]; then + printf 'export CLAUDE_PLUGIN_DATA=%s\n' "$(shell_escape "$CLAUDE_PLUGIN_DATA")" >> "$CLAUDE_ENV_FILE" +fi + +exit 0 From e4d3fe5827b5077d9c4f09e92c92cffa451072ab Mon Sep 17 00:00:00 2001 From: Kevin Turcios Date: Tue, 21 Apr 2026 05:30:19 -0500 Subject: [PATCH 2/3] test: add tests for bash session-start-env.sh hook Four tests verifying the bash replacement matches Node.js behavior: - Basic env var export (session ID + plugin data dir) - Shell escaping roundtrip with single quotes - Clean exit without CLAUDE_ENV_FILE - Byte-for-byte parity with the Node.js session-lifecycle-hook --- tests/runtime.test.mjs | 108 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/tests/runtime.test.mjs b/tests/runtime.test.mjs index 90408372..ea730f2c 100644 --- a/tests/runtime.test.mjs +++ b/tests/runtime.test.mjs @@ -535,6 +535,114 @@ test("task --resume-last ignores running tasks from other Claude sessions", () = assert.match(resume.stderr, /No previous Codex task thread was found for this repository\./); }); +const SESSION_START_BASH = path.join(PLUGIN_ROOT, "scripts", "session-start-env.sh"); + +test("bash session start hook exports the same env vars as the Node.js version", () => { + const repo = makeTempDir(); + const envFile = path.join(makeTempDir(), "claude-env.sh"); + fs.writeFileSync(envFile, "", "utf8"); + const pluginDataDir = makeTempDir(); + + const result = run("bash", [SESSION_START_BASH], { + cwd: repo, + env: { + ...process.env, + CLAUDE_ENV_FILE: envFile, + CLAUDE_PLUGIN_DATA: pluginDataDir + }, + input: JSON.stringify({ + session_id: "sess-bash-test", + cwd: repo + }) + }); + + assert.equal(result.status, 0, result.stderr); + assert.equal( + fs.readFileSync(envFile, "utf8"), + `export CODEX_COMPANION_SESSION_ID='sess-bash-test'\nexport CLAUDE_PLUGIN_DATA='${pluginDataDir}'\n` + ); +}); + +test("bash session start hook handles single quotes in session id", () => { + const envFile = path.join(makeTempDir(), "claude-env.sh"); + fs.writeFileSync(envFile, "", "utf8"); + + const result = run("bash", [SESSION_START_BASH], { + env: { + ...process.env, + CLAUDE_ENV_FILE: envFile + }, + input: JSON.stringify({ + session_id: "O'Brien's \"test\"" + }) + }); + + assert.equal(result.status, 0, result.stderr); + const content = fs.readFileSync(envFile, "utf8"); + assert.match(content, /CODEX_COMPANION_SESSION_ID=/); + // Verify the escaped value roundtrips correctly via shell eval + const script = path.join(makeTempDir(), "roundtrip.sh"); + fs.writeFileSync(script, `${content}\nprintf '%s' "$CODEX_COMPANION_SESSION_ID"`, { mode: 0o755 }); + const roundtrip = run("bash", [script]); + assert.equal(roundtrip.stdout, "O'Brien's \"test\""); +}); + +test("bash session start hook exits cleanly without CLAUDE_ENV_FILE", () => { + const result = run("bash", [SESSION_START_BASH], { + env: { + ...process.env, + CLAUDE_ENV_FILE: "" + }, + input: JSON.stringify({ + session_id: "sess-no-env-file" + }) + }); + + assert.equal(result.status, 0, result.stderr); + assert.equal(result.stdout, ""); +}); + +test("bash session start hook produces identical output to the Node.js version", () => { + const repo = makeTempDir(); + const nodeEnvFile = path.join(makeTempDir(), "node-env.sh"); + const bashEnvFile = path.join(makeTempDir(), "bash-env.sh"); + fs.writeFileSync(nodeEnvFile, "", "utf8"); + fs.writeFileSync(bashEnvFile, "", "utf8"); + const pluginDataDir = makeTempDir(); + const inputJson = JSON.stringify({ + hook_event_name: "SessionStart", + session_id: "sess-parity-check", + cwd: repo + }); + + const nodeResult = run("node", [SESSION_HOOK, "SessionStart"], { + cwd: repo, + env: { + ...process.env, + CLAUDE_ENV_FILE: nodeEnvFile, + CLAUDE_PLUGIN_DATA: pluginDataDir + }, + input: inputJson + }); + + const bashResult = run("bash", [SESSION_START_BASH], { + cwd: repo, + env: { + ...process.env, + CLAUDE_ENV_FILE: bashEnvFile, + CLAUDE_PLUGIN_DATA: pluginDataDir + }, + input: inputJson + }); + + assert.equal(nodeResult.status, 0, nodeResult.stderr); + assert.equal(bashResult.status, 0, bashResult.stderr); + assert.equal( + fs.readFileSync(bashEnvFile, "utf8"), + fs.readFileSync(nodeEnvFile, "utf8") + ); +}); + test("session start hook exports the Claude session id and plugin data dir for later commands", () => { const repo = makeTempDir(); const envFile = path.join(makeTempDir(), "claude-env.sh"); From bce5e9094082bc4fd220f0b2783cc2c1b03ee38a Mon Sep 17 00:00:00 2001 From: Kevin Turcios Date: Tue, 21 Apr 2026 05:32:21 -0500 Subject: [PATCH 3/3] fix: add set -e so write failures propagate as non-zero exit Addresses review feedback: the Node.js version surfaces append failures via uncaught exception, so the bash replacement should propagate them too via set -e. --- plugins/codex/scripts/session-start-env.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/codex/scripts/session-start-env.sh b/plugins/codex/scripts/session-start-env.sh index fdbc9b5b..08654bc6 100755 --- a/plugins/codex/scripts/session-start-env.sh +++ b/plugins/codex/scripts/session-start-env.sh @@ -3,7 +3,7 @@ # Replaces the Node.js session-lifecycle-hook.mjs for the SessionStart path # to avoid ~100ms V8 startup overhead on every session. -set -uo pipefail +set -euo pipefail INPUT=$(cat) SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty' 2>/dev/null)