I tried verifying all patterns of Claude Code's SessionStart hook (Windows 11 + MINGW64)

I tried verifying all patterns of Claude Code's SessionStart hook (Windows 11 + MINGW64)

About Claude Code's SessionStart hook, I comprehensively tested combinations of 4 types of matchers and 3 configuration file locations in a Windows 11 + MINGW64 environment. I'm sharing the verification results, including behaviors not documented.
2026.03.10

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.

auto-compact message

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

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 variable CLAUDE_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:

  1. Output information to stdout to check if it is injected into Claude's context
  2. Write environment variables to CLAUDE_ENV_FILE and check if they can be referenced by executing shell commands from Claude Code
  3. 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:

  1. Start a new session with the claude command
  2. Ask Claude "Can you see the message injected from the SessionStart hook?"
  3. Ask Claude to "Execute echo $TEST_HOOK_VAR" to verify environment variable reflection
  4. Execute /clear
  5. Execute /compact
  6. 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.

Share this article

FacebookHatena blogX

Related articles