/clear changes session_id in hook input without re-running SessionStart hooks, breaking session-aware PreToolUse hooks

Open 💬 3 comments Opened Jun 24, 2026 by mcmanustfj

Summary

When /clear is run, regenerateSessionId() assigns a new UUID to the session. Subsequent PreToolUse hook invocations receive this new session_id in their JSON input. However, SessionStart hooks do not re-run after /clear, so any state created by SessionStart (temp directories, env vars written to CLAUDE_ENV_FILE, permits) is keyed to the original session ID.

This means hooks that validate paths against session_id (e.g., checking that a file is within /tmp/claude-session-{session_id}/) silently break after /clear -- the hook computes an expected path from the new session ID that doesn't match the actual path created under the original session ID.

Reproduction

  1. Create a SessionStart hook that creates a session-scoped temp directory from session_id and exports it via CLAUDE_ENV_FILE.
  2. Create a PreToolUse hook (matcher: Read|Write|Edit, if: Read(/tmp/*)) that validates /tmp/ paths are within /tmp/claude-session-{session_id}/.
  3. Start a session. SessionStart creates /tmp/claude-session-{UUID-A}/ and sets /tmp/claude-session-cb294609-d25f-4354-bc59-cda9ede72fdb.
  4. Write a file via Bash: echo test > /tmp/claude-session-cb294609-d25f-4354-bc59-cda9ede72fdb/data.json (succeeds).
  5. Read the file: Read(/tmp/claude-session-cb294609-d25f-4354-bc59-cda9ede72fdb/data.json) (succeeds -- hook sees session_id = UUID-A, path matches).
  6. Run /clear.
  7. Read the same file: Read(/tmp/claude-session-{UUID-A}/data.json) -- hook now receives session_id = UUID-B (the regenerated ID). The hook computes expected dir as /tmp/claude-session-{UUID-B}/, path doesn't match, hook denies.
  8. cat /tmp/claude-session-{UUID-A}/data.json via Bash still succeeds (Bash tool doesn't go through the Read hook).

Root cause

Three things interact:

  1. /clear regenerates the session ID. regenerateSessionId() is called during /clear, and createBaseHookInput() uses getSessionId() to populate session_id, so all subsequent hooks see the new ID.
  1. SessionStart hooks don't re-run after /clear. They are explicitly skipped for non-startup scenarios. The /tmp/claude-session-cb294609-d25f-4354-bc59-cda9ede72fdb env var persists in Bash tool commands (via CLAUDE_ENV_FILE), but the hook infrastructure has no equivalent mechanism.
  1. Session env vars are not available in hook subprocesses. Hooks inherit process.env via subprocessEnv(), which does not include vars written to CLAUDE_ENV_FILE. So a hook can't read /tmp/claude-session-cb294609-d25f-4354-bc59-cda9ede72fdb to find the correct directory -- it can only derive it from session_id, which is now wrong.

The parent session ID is stored internally (STATE.parentSessionId) but is not included in hook input, so hooks have no way to recover the original session's ID.

(Source references based on the leaked source from 2026-03-31, roughly corresponding to CLI version 2.1.x. Line numbers may have drifted.)

Impact

Any PreToolUse hook that uses session_id to validate or construct paths breaks silently after /clear. The same issue likely affects --continue and --resume, which also skip SessionStart hooks.

Possible fixes

  1. Include session env vars in hook subprocess environment -- source CLAUDE_ENV_FILE content into the env passed to spawn() so hooks can read /tmp/claude-session-cb294609-d25f-4354-bc59-cda9ede72fdb directly.
  2. Add a process_session_id field to hook input that never changes for the lifetime of the process (the original session ID from startup), distinct from the conversation-scoped session_id.
  3. Include parent_session_id in hook input -- let hooks check both current and parent session dirs. Doesn't solve multiple /clears in a row. (Previously requested in #29094, closed as stale.)
  4. Re-run SessionStart hooks after /clear with the new session ID.

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗