Skip to content
Open
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 plugins/codex/hooks/hooks.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
]
Expand Down
25 changes: 25 additions & 0 deletions plugins/codex/scripts/session-start-env.sh
Original file line number Diff line number Diff line change
@@ -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 -euo pipefail

INPUT=$(cat)
SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // empty' 2>/dev/null)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid requiring jq in SessionStart hook

This change introduces a hard runtime dependency on jq for every SessionStart via SESSION_ID=$(... | jq ...); with set -euo pipefail, systems lacking jq now fail the hook with exit 127 before writing CODEX_COMPANION_SESSION_ID/CLAUDE_PLUGIN_DATA. The previous Node.js path did not require jq, and the repo requirements only document Node, so this is a real compatibility regression that can break plugin startup on otherwise supported environments.

Useful? React with 👍 / 👎.


[ -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
Comment thread
KRRT7 marked this conversation as resolved.
108 changes: 108 additions & 0 deletions tests/runtime.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down