
I tried verifying all patterns of Claude Code's SessionStart hook (Windows 11 + MINGW64)
This page has been translated by machine translation. View original
Introduction
When working on long development sessions with Claude Code, the context window approaches its limit and auto-compact runs. Auto-compact is a feature that summarizes and compresses past conversations, but this compression can result in the loss of important context during work.

Static information that can be written in advance in CLAUDE.md is preserved after compaction. However, CLAUDE.md cannot handle the following types of dynamic information:
- Current branch and recent commit history
- Status of ongoing tasks and unresolved issues
- Project-specific rules and policies communicated during the session
The SessionStart hook is a mechanism to address this problem. It allows the hook to fire not only at the start of a session but also after /compact or /clear, reinserting necessary information into Claude's context. This article reports the results of actually running all combinations of the 4 types of matchers and 3 configuration file locations for SessionStart hooks.
Testing Environment
| Item | Version |
|---|---|
| OS | Windows 11 |
| Shell | GNU bash 5.2.37 (MINGW64) |
| Claude Code | 2.1.72 |
| Model | claude-opus-4-6 |
Target Audience
- Engineers using Claude Code for work or personal development
- Those interested in Claude Code's hook functionality but haven't tried it yet
- Those who want to know the specific behavior of SessionStart hooks
References
- Hooks Reference - Claude Code Official Documentation
- How Claude Code Works - Claude Code Official Documentation
SessionStart Hook Mechanism
The SessionStart hook is a mechanism that executes shell commands triggered by events such as session start or resume. Configuration is described in settings.json.
Matchers
SessionStart hooks have 4 types of matchers that can trigger different hooks depending on how the session is started.
| Matcher | Trigger Condition |
|---|---|
startup |
When starting a new session with the claude command |
resume |
When resuming a session with claude --resume or claude --continue |
clear |
When executing the /clear command within a session |
compact |
When manually executing /compact or when auto-compact runs |
Configuration File Locations
Configuration files can be placed in 3 locations.
| Location | Scope | Git Management |
|---|---|---|
~/.claude/settings.json |
Common across all projects | Not possible |
.claude/settings.json |
Project-specific | Possible |
.claude/settings.local.json |
Project-specific | Not possible (local only) |
Main Features
SessionStart hooks provide the following main features:
-
Context injection via stdout
The stdout output of the hook script is injected into Claude's context. Claude can recognize this content and use it in responses. -
Environment variable persistence via CLAUDE_ENV_FILE
Through the environment variableCLAUDE_ENV_FILE, available only in SessionStart hooks, you can set environment variables that are effective throughout the session. -
Processing branching via matchers
By specifying different hook scripts or commands for each matcher, you can branch processing based on how the session is started.
Verification
For verification, I created a hook script that tests three features simultaneously.
#!/bin/bash
MATCHER="${1:-unknown}"
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
LOG_DIR="${CLAUDE_PROJECT_DIR:-.}/.logs"
mkdir -p "$LOG_DIR"
# Read hook input JSON from stdin
INPUT=$(cat)
# Log recording
{
echo "[$TIMESTAMP] matcher=$MATCHER"
echo "$INPUT"
} >> "$LOG_DIR/session-start.log"
# stdout context injection test
cat <<CONTEXT
[SessionStart Hook]
matcher: $MATCHER
timestamp: $TIMESTAMP
project_dir: $CLAUDE_PROJECT_DIR
message: This text was injected by the SessionStart hook.
CONTEXT
# CLAUDE_ENV_FILE environment variable persistence test
if [ -n "$CLAUDE_ENV_FILE" ]; then
{
echo "export TEST_HOOK_MATCHER='$MATCHER'"
echo "export TEST_HOOK_TIMESTAMP='$TIMESTAMP'"
echo "export TEST_HOOK_VAR='hello_from_session_start'"
} >> "$CLAUDE_ENV_FILE"
echo "env_file: variables written to CLAUDE_ENV_FILE"
else
echo "env_file: CLAUDE_ENV_FILE is not set"
fi
exit 0
This script performs three verifications simultaneously:
- Output information to stdout to check if it is injected into Claude's context
- Write environment variables to
CLAUDE_ENV_FILEand check if they can be referenced by executing shell commands from Claude Code - Record the input JSON and matcher name in a log file to verify the firing timing
In the configuration file, I described settings to execute this script for each of the four types of matchers.
{
"hooks": {
"SessionStart": [
{
"matcher": "startup",
"hooks": [
{
"type": "command",
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/session-start.sh\" startup"
}
]
},
{
"matcher": "resume",
"hooks": [
{
"type": "command",
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/session-start.sh\" resume"
}
]
},
{
"matcher": "clear",
"hooks": [
{
"type": "command",
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/session-start.sh\" clear"
}
]
},
{
"matcher": "compact",
"hooks": [
{
"type": "command",
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/session-start.sh\" compact"
}
]
}
]
}
}
Verification Procedure
I prepared separate project directories for each configuration file location and repeated the following steps:
- Start a new session with the
claudecommand - Ask Claude "Can you see the message injected from the SessionStart hook?"
- Ask Claude to "Execute
echo $TEST_HOOK_VAR" to verify environment variable reflection - Execute
/clear - Execute
/compact - End the session and restart with
claude --continue
I checked log files and transcripts at each step and recorded the hook firing status and input JSON content.
Results
Behavior by Matcher
I confirmed that all four types of matchers fired at the expected timing. However, there were differences in the structure of the input JSON for some matchers.
The following is the JSON received by the hook script via stdin when the startup matcher fired:
{
"session_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"transcript_path": "C:\\Users\\<user>\\.claude\\projects\\...\\<session>.jsonl",
"cwd": "D:\\work\\<project-root>\\projects\\test-local",
"hook_event_name": "SessionStart",
"source": "startup",
"model": "claude-opus-4-6"
}
The differences in input JSON by matcher are summarized below:
| Field | startup | clear | compact | resume |
|---|---|---|---|---|
session_id |
New | New | Unchanged | Continuation source |
source |
"startup" |
"clear" |
"compact" |
"resume" |
model |
Present | Absent | Present | Absent |
hook_event_name |
Present | Present | Present | Present |
cwd |
Present | Present | Present | Present |
transcript_path |
Present | Present | Present | Present |
The absence of the model field in clear and resume differs from the official documentation. Additionally, the permission_mode field mentioned in the documentation was not included in any of the matchers.
Context Injection via stdout
I confirmed that the stdout output of the hook was incorporated into Claude's context and referenced in responses. When I asked Claude "Can you see the message injected from the SessionStart hook?", it correctly recognized the matcher, timestamp, project_dir, and message values output by the hook.
In the transcript, it was recorded in a form equivalent to a system-reminder.
Environment Variable Persistence via CLAUDE_ENV_FILE
Writing to CLAUDE_ENV_FILE from the hook script was successful. This was also recorded in the log as "CLAUDE_ENV_FILE written (matcher=startup)".
However, when I executed echo $TEST_HOOK_VAR in the session, the result was empty. This behavior was the same across all three configuration file locations, so the difference in configuration file location does not seem to be the cause. However, there is still a possibility due to differences in OS, shell, or Claude Code execution path. Note that operation on macOS or Linux environments has not been verified.
Differences by Configuration File Location
The behavior was identical across all three locations.
| Aspect | settings.local.json | settings.json | ~/.claude/settings.json |
|---|---|---|---|
| Hook execution | All matchers OK | All matchers OK | All matchers OK |
| Context injection | OK | OK | OK |
| Environment variable persistence | NG | NG | NG |
| Matcher operation | All 4 types fired | All 4 types fired | All 4 types fired |
The configuration file location does not affect the hook firing timing, input JSON structure, or stdout context injection behavior.
Behaviors Not Documented
In the test environment, I confirmed the following four behaviors that differ from the official documentation. These may be specific to Windows + MINGW64 environments.
1. Double Firing When Executing --continue
When resuming a session with claude --continue, I confirmed that the startup matcher fires in addition to the resume matcher. They are recorded at the same time in the log, and at least in practical use, they need to be treated as double firing.
The startup side has a new session ID, while the resume side has the continuation source's session ID. If you want to limit processing to just the resume matcher, you need to design with this double firing in mind.
2. Inconsistency of the model Field
The model field in the input JSON is included in startup and compact but not in clear and resume. If you reference the model name in your hook script, you need to consider cases where this field does not exist.
3. Absence of the permission_mode Field
The official documentation states that the input JSON includes a permission_mode field, but it was not included in any of the matchers in this verification.
4. Session ID Update by /clear
Executing /clear issues a new session ID and creates a new transcript file. On the other hand, /compact does not change the session ID. The following diagram shows the transition of session IDs in a series of operations:
Conclusion
SessionStart hooks are an effective mechanism for addressing context loss due to auto-compact or session resume. All four types of matchers fired normally, and context injection via stdout worked as expected. There was no difference in behavior based on configuration file location.
On the other hand, multiple behaviors that differ from the official documentation were confirmed, such as the double firing of startup and resume at --continue and the absence of the model field. When utilizing SessionStart hooks, I recommend designing with these behaviors in mind.
