Skip to content

fix(mcp): bind omitted session_id to the host Claude process - #1954

Merged
dnlrsls merged 5 commits into
Gentleman-Programming:mainfrom
doblas:fix/1693-bind-mcp-session-to-host-process
Oct 21, 2026
Merged

dnlrsls merged 5 commits into
Gentleman-Programming:mainfrom
doblas:fix/1693-bind-mcp-session-to-host-process

Conversation

@doblas

@doblas doblas commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

🔗 Linked Issue

Closes #1693

Draft for coordination. #1693 is approved and assigned to dnlrsls. The maintainer follow-up adds fail-closed hook handling and invalid-binding rejection. This PR remains a draft; no merge or release is requested.

Lifecycle Evidence


🏷️ PR Type

  • type:bug — Bug fix
  • type:feature — New feature
  • type:question — Question requiring tracked work
  • type:docs — Documentation only
  • type:refactor — Code refactoring (no behavior change)
  • type:chore — Maintenance, dependencies, tooling
  • type:breaking-change — Breaking change

📝 Summary

  • A dedicated exec-form SessionStart hook ("args": ["hook", "claude-bind-session"], on startup, resume, clear and fork) records host process → session under <data dir>/host-sessions/<pid>.json. The record is keyed by PID plus process start time, so a reused PID never matches. Exec form means no shell, so the hook's parent is the Claude process on Linux, macOS and Windows. This was measured; see Notes.
  • When a write omits session_id, engram mcp first looks up its parent's binding. It uses the binding only when the bound session, or its live :resume:N continuation, belongs to the write's project.
  • Only an absent verified process binding retains the existing project/directory fallback. A verified binding that names an unknown, foreign-project, or ended session without a live continuation now rejects omitted-ID saves, summaries, prompts and passive captures without writing. Explicit session_id handling is unchanged.
  • The mutating PreToolUse hook sets onFailure: "block". Supported hook-failure protection requires Claude Code ≥ 2.1.295 and plugin ≥ 0.1.16. A failed SessionStart refresh may retain an old binding; the PID registry alone is not write-time freshness authority.

📂 Changes

File Change
internal/hostbind/* New package: read and write bindings, and get process start time on Linux (/proc), on other Unix systems (ps -o lstart) and on Windows (GetProcessTimes)
cmd/engram/hook_bind.go New engram hook claude-bind-session command (host = os.Getppid(), prints nothing, always exits 0)
cmd/engram/hook_claude.go Add the new hook to usage (the PreToolUse guard is unchanged)
plugin/claude-code/hooks/hooks.json New exec-form SessionStart group; mutating PreToolUse now blocks on hook failure
internal/mcp/mcp.go Resolve host binding first; verified-but-unresolvable bindings return an error before fallback
plugin/claude-code/.claude-plugin/plugin.json, .claude-plugin/marketplace.json 0.1.14 → 0.1.16 (follow-up: 0.1.15 → 0.1.16)
docs/PLUGINS.md Document the host-process binding
*_test.go PID reuse, /clear rebinding, resume continuations, explicit session_id, unusable bindings → refusal; bind uses the parent PID; PreToolUse does not publish; hooks.json exec-form entry

🧪 Test Plan

Maintainer follow-up (864210fa, a28142d4)

  • Gate RED reproduced missing onFailure; focused configuration checks: 4 PASS. Existing hook-command compatibility selection: 8 PASS.
  • Resolver RED reproduced save and summary writing into the wrong sole live session; focused regression/compatibility selection: 13 PASS, including all four writers, unchanged absent-binding behavior, refusals and persisted counters/session records.
  • go test ./plugin -count=1 -timeout=10m -v: 77 PASS / 1 FAIL / 3 top-level SKIP, plus 2 nested skips. The sole failure (TestClaudeHookResponseValidation/jq16=false/valid_project, CRLF versus LF) was reproduced once on pristine 4eeed4a6 with byte-identical helper/test files. It remains unfixed and disclosed; this is not an all-green suite or CI waiver.
  • go test ./internal/mcp -count=1 -timeout=10m -v: 402 PASS / 1 FAIL / 1 SKIP. Sole failure: generated contract byte comparison after Windows checkout converted LF to CRLF. Local LF-only normalization preserved the exact committed blob; go test ./internal/mcp -run '^TestAgentContractArtifactCurrent- [x] Focused regression: go test ./internal/hostbind ./internal/mcp` — ok
  • Affected package tests: go test ./cmd/engram — ok except TestHookSessionConfirmationLatency/*/registration_timeout, which also fails intermittently on a clean origin/main (2 of 3 runs on the branch, 1 of 3 on main, the same unexpected endpoint: / failure, also in the Codex variant this PR doesn't touch)
  • Other checks: go vet on the touched packages; go test ./plugin/... and ./internal/setup pass; make deadcode-check passes; GOOS=windows go build ./... and GOOS=darwin go build ./... pass
  • End to end (Linux): real Claude Code 2.1.302 with the exec-form hook pointing at this branch's engram, and a fake Messages API. host-sessions/<pid>.json was written with the PID and start time of the claude process that is the MCP server's parent, and with the session's ID. Bindings of exited hosts are pruned.
  • Platform wiring: doblas/engram-1693-probe runs real Claude Code 2.1.304 on GitHub Actions on Windows, macOS and Ubuntu, with npm and native installs. Exec-form hooks are direct children of the MCP server's parent on all 6 combinations; shell-form hooks are not on Ubuntu and Windows.

/clear is covered too: one headless process gets prompt, /clear and prompt over --input-format stream-json. On all 6 combinations the exec-form SessionStart (source=clear, new session ID) is a direct child of the MCP server's parent, and the MCP server is not restarted (run 39384361663).


✅ Contributor Checklist

  • I linked an approved, assigned issue above (Closes #1693).
  • This PR has exactly one type:* label: type:bug.
  • I recorded actual focused regression and affected package test commands/outcomes for behavior changes, or N/A for docs-only changes
  • I recorded additional applicable local checks for an unpushed/no-PR or high-risk change, and identified any missing CI evidence
  • Docs updated (if behavior changed)
  • Commits follow conventional commits format
  • No Co-Authored-By trailers in commits
  • I checked every changed path against the Transient Artifact Policy

💬 Notes for Reviewers

  • Size: the original contribution was about 930 changed lines, already over the 400 budget. The follow-up is two separate work-unit commits (50 and 218 changed lines). The cumulative PR remains oversized; this update preserves contributor history and grants no size exception. The proposed original package/integration split remains a review-coordination decision.
  • Platforms: contributor wiring probes cover Windows, macOS and Ubuntu. Supported hook-failure protection now requires Claude Code ≥ 2.1.295 and plugin ≥ 0.1.16. Older hosts must upgrade; exec-form support alone (2.1.139) is insufficient. Bare MCP and disabled/unmatched hooks do not provide plugin hook-failure protection.
  • Ownership: coordinating with @BraisonCrece in It could not be saved to Engram because there is another active session on the same project. #1693 so there is a single implementation.
  • Kept separate from fix(claude): register a resumable session after compaction #1935 (compaction) and bug(hooks): with an external Engram server, Claude/Codex hooks register sessions under the server-side folder-name project, so explicit writes to the real project are denied (session_project_mismatch) #1911 (external-server project resolution).
    -count=1 -v` then passed. No generated JSON change is committed; the full suite was not rerun or relabeled green.
  • Disposable public-handler differential against 864210fa: 24 cases per version, then 12 exact normalized compatibility comparisons + 12 intended no-write refusals. Only generated sync/review metadata and verified physical config-source checkout roots were normalized. Session paths and persisted effects remained exact. Comparator fixture defects were corrected under explicit authorization using saved reports, without rerunning Go or changing product expectations.
  • Independent source-and-retained-evidence verification: qualified PASS for both follow-up units. Native R4 targeted validation and exact acknowledgement closed for 864210fa; its receipt does not cover the resolver commit. Resolver ASSESS: native medium, escalated high/shared contract item 3, reviewDue=false; required independent verification completed.
  • Published head a28142d4: all six required checks passed, as did lint, Windows setup/cloud-wrapper checks, policy helpers and Obsidian build (functional CI run 39543766702). The non-required issue-auto-assignment job failed with only Unable to load or run trusted issue assignment helper.; no specific cause was exposed and no rerun or workflow change was made here. Older label/size runs were cancelled and superseded by successful runs. Performance Ratchet was skipped; CodeRabbit's success does not imply a technical review of this draft.
  • The requested final frozen PR snapshot was separately approved by all four native review lenses and its exact acknowledgement completed (review-353d702609064f0c, target sha256:b1b3eb3355b2ff8a288c2ec49955b0cafaad6fecb16a74a14726884734c1d3b5). Its informational R3-prune-publication-race warning is non-blocking later work, not an open correction. This is separate from the earlier R4 receipt.
  • Actual Claude hook-failure enforcement remains unverified locally; the supported-host guarantee relies on the documented host contract, not a simulated host test. This PR remains a draft; no merge or release was performed.

Original contributor verification (before this follow-up; historical)

  • Focused regression: go test ./internal/hostbind ./internal/mcp — ok
  • Affected package tests: go test ./cmd/engram — ok except TestHookSessionConfirmationLatency/*/registration_timeout, which also fails intermittently on a clean origin/main (2 of 3 runs on the branch, 1 of 3 on main, the same unexpected endpoint: / failure, also in the Codex variant this PR doesn't touch)
  • Other checks: go vet on the touched packages; go test ./plugin/... and ./internal/setup pass; make deadcode-check passes; GOOS=windows go build ./... and GOOS=darwin go build ./... pass
  • End to end (Linux): real Claude Code 2.1.302 with the exec-form hook pointing at this branch's engram, and a fake Messages API. host-sessions/<pid>.json was written with the PID and start time of the claude process that is the MCP server's parent, and with the session's ID. Bindings of exited hosts are pruned.
  • Platform wiring: doblas/engram-1693-probe runs real Claude Code 2.1.304 on GitHub Actions on Windows, macOS and Ubuntu, with npm and native installs. Exec-form hooks are direct children of the MCP server's parent on all 6 combinations; shell-form hooks are not on Ubuntu and Windows.

/clear is covered too: one headless process gets prompt, /clear and prompt over --input-format stream-json. On all 6 combinations the exec-form SessionStart (source=clear, new session ID) is a direct child of the MCP server's parent, and the MCP server is not restarted (run 39384361663).


✅ Contributor Checklist

  • I linked an approved, assigned issue above (Closes #1693).
  • This PR has exactly one type:* label: type:bug.
  • I recorded actual focused regression and affected package test commands/outcomes for behavior changes, or N/A for docs-only changes
  • I recorded additional applicable local checks for an unpushed/no-PR or high-risk change, and identified any missing CI evidence
  • Docs updated (if behavior changed)
  • Commits follow conventional commits format
  • No Co-Authored-By trailers in commits
  • I checked every changed path against the Transient Artifact Policy

💬 Notes for Reviewers

Summary by CodeRabbit

  • New Features

    • Claude Code sessions now remain associated across startup, resume, clear, and fork events.
    • Session-aware operations can identify the correct live session when no session ID is provided.
    • Session associations are validated against active host processes and live sessions.
  • Bug Fixes

    • Claude Code tool actions are blocked when Engram hooks fail, time out, return invalid output, or are denied.
    • Invalid or stale associations no longer route data to the wrong session.
  • Documentation

    • Updated Claude Code setup requirements and documented session binding and hook-failure protection.
  • Chores

    • Updated the Engram Claude plugin to version 0.1.16.

When the Claude PreToolUse hook is cancelled at its 5s timeout, mem_save
reaches the MCP server without a session_id and the fallback refuses
because two live sessions share the project and directory.

SessionStart (startup, resume, clear, fork) and each successful PreToolUse
confirmation now record host process -> session under the data dir, keyed
by PID plus process start time so a reused PID never matches. The MCP
server resolves an omitted session_id through its parent's binding when
the bound session, or its live :resume:N continuation, is in the write's
project. Otherwise the existing fail-closed ambiguity refusal applies.
Explicit session IDs and non-Claude clients are unchanged.

Refs Gentleman-Programming#1693
@coderabbitai

coderabbitai Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: Gentleman-Programming/engram/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 47531dbf-56e5-46e9-883f-ae70033729e6
📥 Commits

Reviewing files that changed from the base of the PR and between a28142d and 1900909.

📒 Files selected for processing: 4
  • cmd/engram/hook_claude.go
  • cmd/engram/hook_usage_test.go
  • cmd/engram/pi_env_main_test.go
  • docs/PLUGINS.md

Included review availability: This review used your included allowance. Your plan provides up to 8 included reviews per hour; 6 remain after this review.

📝 Walkthrough

The change adds process-validated host-to-session bindings for Claude Code. SessionStart hooks publish bindings, and MCP uses verified bindings to select sessions when writes omit session_id. The plugin also blocks matched tool calls when hooks fail.

Changes: Host-session binding and Claude hook enforcement

Layer / File(s) Summary
Persist and validate host bindings
internal/hostbind/*
Bindings are stored by PID with a process start token. Lookup rejects invalid or stale records, and publishing replaces bindings and prunes stale entries. Process start identity is implemented for Linux, other Unix systems, and Windows.
Publish bindings from Claude hooks
cmd/engram/hook_bind.go, cmd/engram/hook_claude.go, cmd/engram/hook_bind_test.go, cmd/engram/pi_env_main_test.go, plugin/claude-code/hooks/hooks.json, plugin/claude-code/.claude-plugin/plugin.json, .claude-plugin/marketplace.json, plugin/claude_code_hook_enforcement_test.go, docs/PLUGINS.md
The SessionStart hook runs the new claude-bind-session command for startup, resume, clear, and fork events. The command publishes a binding when it receives a valid session ID and host PID. PreToolUse calls are configured to block when the hook fails. Plugin versions and setup documentation are updated.
Resolve omitted MCP session IDs
internal/mcp/mcp.go, internal/mcp/host_binding_test.go
MCP validates a host binding and its session before using it for writes without session_id. An unusable verified binding returns an error rather than falling back. Existing fallback behavior remains when no binding exists.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix · Severity of issue fixed: Medium

Sequence Diagram(s)

sequenceDiagram
  participant ClaudeCode
  participant EngramCLI
  participant HostBindings
  participant MCPClient
  participant MCPServer
  ClaudeCode->>EngramCLI: SessionStart sends session_id
  EngramCLI->>HostBindings: Publish binding for parent PID
  MCPClient->>MCPServer: Write without session_id
  MCPServer->>HostBindings: Lookup binding for parent PID
  HostBindings-->>MCPServer: Return verified session binding
  MCPServer-->>MCPClient: Resolve write to live session
Loading

Suggested reviewers: gentleman-programming, dnlrsls

Merge Risk: ⚪ Minimal · up to 19009

No identified issue blocks merging after normal checks.

Pre-merge checks: Passed 4
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the primary change: binding omitted MCP session IDs to the host Claude process.
Linked Issues check Passed Issue #1693 requires saves from different Claude sessions in the same project to succeed. plugin/claude-code/hooks/hooks.json adds an exec-form SessionStart binding hook. internal/mcp/mcp.go res…
Out of Scope Changes check Passed The changes remain connected to #1693. The host identity package, cross-platform process-start checks, fail-closed PreToolUse configuration, plugin version update, documentation, and regression test…
Full details: Linked Issues check

Explanation

Issue #1693 requires saves from different Claude sessions in the same project to succeed. plugin/claude-code/hooks/hooks.json adds an exec-form SessionStart binding hook. internal/mcp/mcp.go resolves an omitted session_id from the verified parent-process binding before fallback. The reported tests cover ambiguous same-project sessions, rebinding, continuations, omitted-ID writers, refusal cases, and explicit IDs. The implementation also preserves fallback behavior when no verified binding exists.

Full details: Out of Scope Changes check

Explanation

The changes remain connected to #1693. The host identity package, cross-platform process-start checks, fail-closed PreToolUse configuration, plugin version update, documentation, and regression tests support correct host-session attribution or prevent unsafe fallback. The broader platform support does not establish unrelated product behavior.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

doblas and others added 3 commits October 20, 2026 09:00
…hook

Measured on GitHub Actions (Claude Code 2.1.304, Windows, macOS and
Ubuntu, npm and native installs): shell-form hooks run through sh -c or
Git Bash, so on Ubuntu (dash) and Windows a bash script's $PPID and a
shell-form executable's os.Getppid() are an intermediate shell, not the
Claude process. Exec-form hooks (args set, no shell, Claude Code 2.1.139+)
are direct children of Claude on every platform tested.

Publish the binding from a dedicated exec-form SessionStart hook using
os.Getppid(), drop ENGRAM_HOST_PID from session-start.sh, and stop
publishing from the shell-form PreToolUse hook, which is left unchanged
so older Claude Code versions keep working.

Evidence: https://github.com/doblas/engram-1693-probe

Refs Gentleman-Programming#1693
Require Claude Code 2.1.295 hook-failure semantics so a failed or cancelled identity rewrite cannot fall through to a stale host binding. Keep hook matching, command and timeout unchanged. Document the host requirement and advance paired plugin metadata.

Refs Gentleman-Programming#1693
Keep absent bindings on the existing fallback path, but propagate verified host-binding failures before any save, summary, prompt or passive-capture write can be attributed to another live session. Cover unchanged fallback and explicit-session behavior alongside no-write rejection.

Refs Gentleman-Programming#1693
@dnlrsls dnlrsls added the type:bug Bug fix label Oct 21, 2026
@dnlrsls dnlrsls changed the title fix(mcp): bind omitted session_id to the host Claude process fix(mcp): bind omitted session_id to the host Claude process [old-base helper smoke] Oct 21, 2026
@dnlrsls dnlrsls changed the title fix(mcp): bind omitted session_id to the host Claude process [old-base helper smoke] fix(mcp): bind omitted session_id to the host Claude process Oct 21, 2026
@dnlrsls
dnlrsls marked this pull request as ready for review October 21, 2026 06:19
Merge canonical main 9795575 into PR1954 while keeping both claude-bind-session and resolve-hook-project. Preserve truthful usage and isolate native hook test bindings lazily so configuration-free subprocesses need no temporary directory.

Keep test processes away from legacy developer database recovery without changing runtime behavior. Refs Gentleman-Programming#1693.

Risk: item 3 (shared hook contracts); item 5 (integration).
@dnlrsls
dnlrsls added this pull request to the merge queue Oct 21, 2026
Merged via the queue into Gentleman-Programming:main with commit 258e7a3 Oct 21, 2026
31 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type:bug Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

It could not be saved to Engram because there is another active session on the same project.

2 participants