Skip to content

Bug: session_id never written to state.md — double-slash path mismatch in PostToolUse signal verification #67

@gyy0592

Description

@gyy0592

Context

Running RLCR on a multi-session setup (two Claude Code instances in separate terminals, same user). The stop hook from the humanize RLCR loop bled into a completely unrelated Claude Code session, hijacking it and causing both instances to interfere with the humanize project.

  • Humanize version: 1.14.0
  • Claude Code version: 2.1.91
  • Platform: Linux (NFS-mounted home directory, ~/.claude is a symlink to NFS path)

Root Cause

The session_id field in state.md is never populated, despite the session_id plumbing (signal file + PostToolUse hook) being present in v1.14.0. This causes the backward-compatibility fallback in find_active_loop() to match any session, breaking session isolation.

The double-slash path mismatch

The failure chain:

  1. CLAUDE_PLUGIN_ROOT has a trailing slash. The Claude Code framework sets it to:

    ~/.claude/plugins/marketplaces/humania/
    
  2. Command template concatenation produces a double slash. commands/start-rlcr-loop.md line 65:

    "${CLAUDE_PLUGIN_ROOT}/scripts/setup-rlcr-loop.sh" $ARGUMENTS

    Resulting command (confirmed from session transcript):

    "~/.claude/plugins/marketplaces/humania//scripts/setup-rlcr-loop.sh" plan.md ...
    

    Note: humania//scriptsdouble slash.

  3. Setup script normalizes its own path. setup-rlcr-loop.sh lines 26, 822-823:

    SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)"
    SCRIPT_SELF_PATH="$SCRIPT_DIR/$(basename "${BASH_SOURCE[0]:-$0}")"

    cd + pwd normalizes the path, removing the double slash:

    ~/.claude/plugins/marketplaces/humania/scripts/setup-rlcr-loop.sh
    

    This normalized path is written to .humanize/.pending-session-id as COMMAND_SIGNATURE.

  4. PostToolUse hook does strict string comparison. loop-post-bash-hook.sh lines 82-86:

    if [[ "$HOOK_COMMAND" == "\"${COMMAND_SIGNATURE}\"" ]] || \
       [[ "$HOOK_COMMAND" == "\"${COMMAND_SIGNATURE}\""[[:space:]]* ]]; then
        IS_SETUP="true"
    • HOOK_COMMAND (from tool_input.command): "...humania//scripts/..." (double slash, as Claude typed it)
    • COMMAND_SIGNATURE (from signal file): "...humania/scripts/..." (single slash, normalized by cd+pwd)
    • Result: no match → exit 0 → signal file not consumed → session_id never written.
  5. Empty session_id matches any session. loop-common.sh line 278:

    if [[ -z "$stored_session_id" ]] || [[ "$stored_session_id" == "$filter_session_id" ]]; then

    Any Claude Code instance's Stop event matches the loop → cross-session hijacking.

Evidence

Signal file still unconsumed (never accessed since creation):

$ stat .humanize/.pending-session-id
Access: 2026-04-03 23:28:47
Modify: 2026-04-03 23:28:47

$ cat .humanize/.pending-session-id
<project_dir>/.humanize/rlcr/2026-04-03_23-28-47/state.md
<home>/.claude/plugins/marketplaces/humania/scripts/setup-rlcr-loop.sh

state.md session_id is empty (all 6 historical loops have the same problem):

session_id:

Session transcript proving the double-slash invocation:

{
  "tool_input": {
    "command": "\"<home>/.claude/plugins/marketplaces/humania//scripts/setup-rlcr-loop.sh\" plan.md --codex-model gpt-5.4:high --max 20 --codex-timeout 10800 --track-plan-file"
  }
}

Reproduction of the matching failure:

COMMAND_SIGNATURE="<home>/.claude/plugins/marketplaces/humania/scripts/setup-rlcr-loop.sh"
HOOK_COMMAND='"<home>/.claude/plugins/marketplaces/humania//scripts/setup-rlcr-loop.sh" plan.md ...'

[[ "$HOOK_COMMAND" == "\"${COMMAND_SIGNATURE}\""* ]]  # → false (double slash ≠ single slash)

Impact

  • All RLCR loops started via /start-rlcr-loop have empty session_id when CLAUDE_PLUGIN_ROOT has a trailing slash.
  • Cross-session stop hook hijacking: any concurrent Claude Code instance's Stop event can claim the RLCR loop.
  • Confirmed on all 6 historical loops in this project (2026-03-27 through 2026-04-03).

Suggested Fixes

Option A: Normalize paths before comparison (minimal, recommended)

In loop-post-bash-hook.sh, normalize both paths before the boundary-aware match:

# Normalize consecutive slashes for robust comparison
COMMAND_SIGNATURE=$(printf '%s' "$COMMAND_SIGNATURE" | tr -s '/')
HOOK_COMMAND=$(printf '%s' "$HOOK_COMMAND" | tr -s '/')

Option B: Use realpath for canonical comparison

COMMAND_SIGNATURE=$(realpath -m "$COMMAND_SIGNATURE" 2>/dev/null || echo "$COMMAND_SIGNATURE")
# And normalize HOOK_COMMAND similarly after extracting the path portion

Option C: Don't depend on command string matching at all

Write session_id directly inside setup-rlcr-loop.sh using $CLAUDE_SESSION_ID env var (if available), or accept the session_id as a CLI argument passed from the command template.

Framework-side fix

Claude Code should strip trailing slashes from CLAUDE_PLUGIN_ROOT before setting it, or normalize the path.

Quantitative Summary

Metric Value
Affected loops 6/6 (100%)
session_id populated 0/6
Signal files consumed 0/6
Cross-session hijack events observed ≥1 confirmed
Root cause 1 character: trailing / in CLAUDE_PLUGIN_ROOT

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions