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)
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 5/5
- Estimated time
- Over a week
- Newbie friendliness
- 15/100
Research direction
Start with the server-side GET /project/current?cwd= call in cmd/engram/hook_claude.go (confirmHookSession) and resolve_project in plugin/claude-code/scripts/_helpers.sh, then trace how the resulting project reaches POST /sessions and the mem_save write gate. Done means that with ENGRAM_URL pointing at an external server, the session is registered with the workspace's real project and an explicit mem_save to that project succeeds. The fix needs a design decision on who resolves the project, so confirm the direction with maintainers first.
Written by the indexing model from the issue text.
Description
Existing Issue Search
- I searched existing issues and will add evidence to a canonical tracker when applicable.
Related, but not the same root/scope:
- #1532 (Streamable HTTP transport): a comment there describes this root cause as a workaround for an HTTP-transport shared server; the proposed
X-Engram-Subprojectheader would apply to HTTP mode only, not to stdio MCP over SSH or HTTP hooks reached throughENGRAM_URL. - #1720 (update/delete ownership checked against the MCP process project): same shared-server family, update/delete only, fixed.
- #1716 / #1717 (PreToolUse write gate when the shell cwd drifts): same gate, single-machine cwd drift.
- #1591 (explicit cross-project saves vs session ownership): fixed for Pi only via satellite sessions.
- #262 / #1656 (explicit
cwd/directoryfor MCP project discovery): still resolved on the server's filesystem.
📝 Observed Consequence
When the Engram server runs on a different machine than the agent workspace (supported via ENGRAM_URL), hook-registered sessions and captured prompts are filed under a project derived from the folder name instead of the workspace's real project.
Because 3.x binds writes to the hook-registered session, an agent's explicit, correct mem_save(project: "<real project>") is denied:
- Claude Code:
session_project_mismatch - Codex: PreToolUse denial "host session registration could not be confirmed"
The only write that succeeds is one that targets the folder-name project, so an agent that retries after the denial is pushed toward filing durable memory in a non-canonical project.
🔄 Steps to Reproduce
- Host B: run
engram serve. Host A: no local server;ENGRAM_URL=http://127.0.0.1:7437forwarded to host B over SSH. MCP configured asssh hostB engram mcp --tools=agent. - Host A: a repository whose canonical Engram project is
myproj(bound via.engram/config.jsonor its git remote), checked out as a worktree folder namedfeature-x. - Start Claude Code (official plugin) in
/home/me/work/myproj-worktrees/feature-x. - SessionStart hook calls
GET /project/current?cwd=/home/me/work/myproj-worktrees/feature-x. Host B cannot see that path and answers{"project":"feature-x","project_source":"dir_basename"}; the hook then sendsPOST /sessions {project:"feature-x", ownership_mode:"project_owned"}. - The agent calls
mem_save(project: "myproj", ...). - Optional:
mem_current_projectreturns the basename of the SSH login directory on host B (e.g.me).
✅ Expected Behavior
In external-server mode, the session/prompt project is resolved where the workspace actually lives (host A), and mem_save(project: "myproj") succeeds.
If host A cannot resolve a project, registration fails closed (registration skipped, or the write denied with an actionable hint) instead of silently adopting a folder name.
❌ Actual Behavior
- The session and its prompts are attributed to
feature-x. mem_save(project: "myproj")returnserror_code: session_project_mismatch("Use a project that matches the existing session, or omit session_id and write to a different project.").- Codex, same setup: PreToolUse denial "Codex host session registration could not be confirmed".
- A write with
project: "feature-x"succeeds.
To be precise about causality: Engram does not move or misfile memory on its own here. In our setup, durable memory ended up in a folder-name project only when an agent, after its correct write was denied, retried with the folder-derived project; Engram accepted that explicit value as designed. The bug is the wrong session attribution and the resulting denial of the correct write.
Operating System
macOS
Release Version (Report Context)
3.0.0 (server and clients). Checked against v3.4.0 and current main source: hooks still resolve via server-side GET /project/current?cwd= (cmd/engram/hook_claude.go, plugin/claude-code/scripts/_helpers.sh).
Agent / Client
Claude Code (official plugin 0.1.5); also observed with Codex (official plugin).
📋 Relevant Logs
GET /project/current?cwd=/home/me/work/myproj-worktrees/feature-x
-> {"project":"feature-x","project_source":"dir_basename", ...}
mem_save(project: "myproj")
-> {"error_code":"session_project_mismatch","hint":"Use a project that matches the existing session, or omit session_id and write to a different project.", ...}
Codex: Tool call blocked by PreToolUse hook: Codex host session registration could not be confirmed. Tool: mcp__engram__mem_save
💡 Additional Context
Why the server's cwd cannot work here. /project/current?cwd= runs the server's own detection (.engram/config.json → git → child repo → basename) against its own filesystem. With the server on another machine the path does not exist there, so every rule except the basename fails, and the basename is often wrong (a worktree name, an agent name, or a repo folder that differs from the project). The MCP process launched over SSH has the same problem: its cwd is the remote login directory, unrelated to the workspace.
Why engram mcp --project / ENGRAM_PROJECT alone is not enough.
- It only sets the MCP process default; hooks still register sessions and prompts over HTTP with a server-resolved project.
- The write gate checks against the hook-registered session, so a correct
--projectMCP default still producessession_project_mismatch. - MCP configs are usually global per host, so one static value would misroute every other repository unless each launch is wrapped.
Proposed direction (smallest change). When ENGRAM_URL points to an external server, resolve the project on the client and send it, instead of asking the server to resolve the client's path:
- Hooks (
_helpers.shresolve_projectand the GoconfirmHookSession) run the localengrambinary's detection on host A (the PreToolUse hook already invokes the local binary) and pass the result in the existingprojectfield ofPOST /sessions/POST /prompts. No new server endpoint is needed for that path. - The server keeps all validation (normalization, known-project rules, ownership); the client value only replaces the path-based detection the server cannot perform.
- No client resolution → fail closed (no folder-name registration) in external mode.
- Optional, for cross-repo work: extend the #1591 satellite-session approach to Claude/Codex, so an explicit write to a different known project uses an isolated project-owned satellite session instead of a denial.
- Optional: an MCP initialization parameter or forwarded env var for the process default (cosmetic once sessions are correct).
Compatibility.
- Opt-in by condition: only when
ENGRAM_URLis external; local single-machine behavior is unchanged. - Server validation is unchanged; an older server receiving a client-resolved
projectbehaves as it does today. - Aligns with #1532's per-request project header, so the HTTP-transport and SSH/tunnel setups share one rule: the project comes from the client.
- Explicitly not proposed: trusting arbitrary client strings without the server's normal validation, or changing session ownership semantics.
- Dominant language
- Go
- Stars
- 7.1k
- Forks
- 729
- Avg merge
- 10h 43m
- Merged PRs (30d)
- 332
Getting set up
Starts the project's dev container in your browser, under your own GitHub account.
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from Gentleman-Programming/engram
-
status:needs-review type:bug
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Gentleman-Programming/engram#1687 ·
Maintainers usually reply within 1 day
-
priority:medium status:needs-review type:feature
Difficulty 2/5 1-3 hours Newbie friendliness 80/100
Gentleman-Programming/engram#863 · 1 comment ·
Maintainers usually reply within 1 day
-
question(sync): isolate enrolled-repair writer blocking in a reproducible fixturePossibly taken @dnlrsls claimed this today. Openstatus:approved type:question
Gentleman-Programming/engram#1913 · 3 comments · 1 assignee ·
Maintainers usually reply within 1 day
-
status:needs-review type:bug
Difficulty 4/5 3-5 days Newbie friendliness 12/100
Gentleman-Programming/engram#1912 ·
Maintainers usually reply within 1 day
-
Difficulty 5/5 Over a week Newbie friendliness 12/100
Gentleman-Programming/engram#1909 · 1 comment ·
Maintainers usually reply within 1 day
All issues in Gentleman-Programming/engram
Similar issues
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
devantler-tech/ksail#7836 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
keyxmakerx/Chronicle#1469 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 73/100
Maintainers usually reply within 1 day
-
bug needs-acceptance wg/router-models-inference-runtime
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
vllm-project/semantic-router#5177 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
Gentleman-Programming/gentle-ai#5565 · 1 comment ·
Maintainers usually reply within 1 day