Repository navigation
fix(hooks): SessionStart prunes orphaned code graphs even when it ingests nothing (#711, slice 2) - #897
Conversation
|
CI autopilot: Run 42122711761, on
Diagnosis:
Fix:
Not verified: locally, without PostgreSQL, Generated by Claude Code |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
Security findingsAdvisory findings (2)
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Claude finished @cdeust's task in 2m 40s —— View job Review 1 of 3 on
|
Review 1 of 3 (ADR-1119): PR #897Five reviews ran on
Notes, no action before merge
Verdict: approve |
There was a problem hiding this comment.
🛡️ Codex Security Review · Automatically triggered
Here are some automated security review suggestions for this pull request.
Reviewed commit: b5a2a45287
ℹ️ About Codex security reviews in GitHub
This is an experimental Codex feature. Security reviews are triggered when:
- You comment "@codex security review"
- A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review
Once complete, Codex will leave suggestions, or a comment if no findings are found.
| orphans = code_graph_cache.orphan_graphs(code_graph_cache.managed_cache_dir()) | ||
| if not orphans: | ||
| return | ||
| cmd = child_command("mcp_server.hooks.code_graph_prune_background") |
There was a problem hiding this comment.
🛡️ Codex Security Review · Automatically triggered
Security: Isolate the prune worker from project imports
When the native Codex wheel is used and the managed cache contains an orphan, this new spawn runs python -m mcp_server.hooks.code_graph_prune_background without -I or a safe cwd. runtime.py isolates the parent but preserves the session's project directory, and spawn_logged preserves it too. A repository containing mcp_server/hooks/code_graph_prune_background.py therefore executes code at SessionStart as the user's account. This materially adds an immediate trigger to the pre-existing unsafe helper whenever stale worktree graphs exist. Pass -I/PYTHONSAFEPATH or set a trusted cwd. Cortex hooks run with the user's full filesystem permissions.
SECURITY.md reference: SECURITY.md:L5-L20
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Confirmed and fixed in the next push (the commit after b5a2a45, fixup! on this PR).
- Traced: without a launcher (Codex wheel runtime),
child_commandreturned[python, "-m", module]and the spawn inherits the hook's cwd, which is the user's project.-mputs the cwd first onsys.path, so a repository shippingmcp_server/hooks/<module>.pyreplaced the worker. The same gap existed before this PR for the consolidate, capture and session-lifecycle children. - Fix:
child_commandnow passes-P(Python 3.11+, the project's floor), which keeps the cwd out ofsys.path, for every child it builds. The launcher path was already safe: there,sys.path[0]is the plugin'sscripts/directory. - Proof:
test_without_a_launcher_a_module_in_the_cwd_is_not_runplants such a package in the cwd. Without the fix, the planted module ran and the test failed; with the fix, the test passes. The local mutation gate reports 5 files, 0 failed.
Generated by Claude Code
There was a problem hiding this comment.
Second half pushed in d058989. spawn_logged also starts the log relay as python -m mcp_server.infrastructure.log_relay from the same inherited cwd, so a planted relay module had the same effect for every logged spawn, launcher or not. relay_command now passes -P. tests_py/infrastructure/test_log_relay_cwd.py plants the module: it ran before the fix and no longer runs.
Generated by Claude Code
…ests nothing (#711, slice 2) Slice 1 (#869) pruned graphs whose recorded root is gone, but only after ingest_codebase had built a graph. A session with a fresh graph, no pipeline or a failed analyze left every deleted worktree's graph (0.6 to 1.1 GB each, measured 2026-10-10) on disk. The SessionStart hook now reads the cortex-root.json markers of the managed cache (~/.cache/cortex/code-graphs) and, when at least one names a root that no longer exists, starts the detached code_graph_prune_background worker, so the removal never runs inside the hook's time budget. A directory without a marker is still reported and kept. Refs #711 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_018kyZYsdTexaL2aS5FFzdeC
b5a2a45 to
a4a577c
Compare
|
Claude finished @cdeust's task in 3m 3s —— View job Review 2 of 3 on
|
Review 2 of 3 (ADR-1119): PR #897Scope. The branch was squashed to Earlier findings. Review 1 raised no Blocker. The Codex P1 is fixed for the worker, but not for the second Python process the same spawn starts (see below). Blocker
How it was traced: the code path was read end to end, and the shadowing works by the same Fix: add Notes, no action before merge
Verdict: block |
… it ingests nothing (#711, slice 2) Codex security review on b5a2a45 (P1), second half: spawn_logged also starts the log relay as [python, -m, mcp_server.infrastructure.log_relay] from the inherited cwd, so a planted relay module in the user's project ran for every logged spawn, launcher or not. relay_command now passes -P. test_log_relay_cwd plants the module and shows it no longer runs (it ran before the fix). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_018kyZYsdTexaL2aS5FFzdeC
|
Claude finished @cdeust's task in 1m 19s —— View job Review 3 of 3 on
|
Review 3 of 3 (ADR-1119): PR #897Scope. I reviewed only Review 2 Blocker: fixed. Not verified: I ran no tests, ruff or craftsmanship checks, because this session's permissions did not allow pytest. The analysis above comes from reading the code. Notes, no action before merge
What can merge: the whole PR, both commits Verdict: approve |
There was a problem hiding this comment.
🛡️ Codex Security Review · Automatically triggered
Here are some automated security review suggestions for this pull request.
Reviewed commit: d058989a9f
ℹ️ About Codex security reviews in GitHub
This is an experimental Codex feature. Security reviews are triggered when:
- You comment "@codex security review"
- A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review
Once complete, Codex will leave suggestions, or a comment if no findings are found.
| """ | ||
| return launcher_child_command(module, *arguments) or [ | ||
| python_executable(), | ||
| "-P", |
There was a problem hiding this comment.
🛡️ Codex Security Review · Automatically triggered
Security: Isolate wheel children from inherited PYTHONPATH
When the native Codex wheel hooks are trusted, an orphan graph exists, and inherited PYTHONPATH contains the project (., its path, or an empty component), use -I instead of -P. spawn_logged passes env=None, and -P suppresses only Python's implicit cwd insertion, so the new SessionStart worker imports a project-planted mcp_server.hooks.code_graph_prune_background. The prior default-cwd path is fixed, but a subprocess reproduction with PYTHONPATH=. is fresh evidence that this worker remains replaceable, yielding code execution as the user. Cortex hooks execute automatically with that user's filesystem permissions.
SECURITY.md reference: SECURITY.md:L5-L16
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Traced, and valid. I am not fixing it in this PR: it has used its three reviews (ADR-1119), so the fix goes to a refinement PR, and the owner decides on this thread.
- Path: the Codex wheel parent runs with
-I(plugins/hypermnesia-mcp-codex/scripts/runtime.py:76-79), so it ignoresPYTHONPATH. Its children, built bychild_commandand the log relay, carry only-Pand inheritPYTHONPATH(env=None). An entry that names the project (., its path, or an empty component) still lets a plantedmcp_server.hooks.<worker>run. The children are therefore less isolated than their parent. - Before this PR: consolidate, capture and session-lifecycle already spawned the same way. This PR adds the prune trigger.
- Proposed refinement:
- In the wheel branch of
child_command, pass-I, the same isolation as the parent. The child imports only from the venv. - For the relay: keep
-P, and stop inheriting the caller'sPYTHONPATH, so it is exactly_PACKAGE_ROOT(_relay_env).-Iwould break the plugin runtime, which finds the package only through that variable. - Tests run each child with
PYTHONPATH=.in a cwd that holds a planted module, plus a positive control without the flag.
- In the wheel branch of
- Owner decision needed: whether a child of the plugin launcher should also stop inheriting
PYTHONPATH. The launcher changes into the plugin root first, so.no longer names the project there. An absolute path to the project inPYTHONPATHstill would.
Generated by Claude Code
… it ingests nothing (#711, slice 2) Mutation gate (ADR-1122), check unit, run 42236098720: the stats run for log_relay.py failed ("failed to collect stats. runner returned 1"). test_log_relay_cwd spawned the real relay with the inherited MUTANT_UNDER_TEST, so mutmut's trampoline ran in the planted project cwd, found no [tool.mutmut] config and died, and out.log was never written. The relay subprocess now drops MUTANT_UNDER_TEST, as tests_py/_hermetic_hook_env.py does; relay_command still runs in the test process, so its mutants are judged. PYTHONSAFEPATH is dropped too, so the test fails without -P (review 3 note). Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_018kyZYsdTexaL2aS5FFzdeC
|
CI autopilot: on Run 42236098720: the suite passed ( Diagnosis, reproduced locally:
Fix: the relay subprocess drops Local receipt: Generated by Claude Code |
Summary
Slice 2 of #711. Slice 1 (#869) ties each graph under
~/.cache/cortex/code-graphsto its project root (cortex-root.json) and removes graphs whose root is gone, but it runs that pruning only afteringest_codebasehas built a graph. A session that ingests nothing (fresh graph, no pipeline installed, failed analyze) leaves every deleted worktree's graph (0.6 to 1.1 GB each, measured 2026-10-10 in the issue) on disk.Root cause: the only caller of
prune_orphan_graphsis the tail of a successful ingest (ingest_codebase._prune_precedent_graphs).Change:
code_graph_cache.orphan_graphs(cache_dir)lists the orphans by reading the markers, without deleting anything;prune_orphan_graphsnow uses the same classification._maybe_background_prune_graphs) calls it on the managed cache. When at least one orphan exists, it starts the new detachedmcp_server.hooks.code_graph_prune_backgroundworker (log:~/.claude/methodology/code_graph_prune.log), sormtreenever runs inside the hook's time budget. Failures are logged, never raised.b5a2a45, P1): without a launcher,child_commandran[python, "-m", module]from the hook's cwd, which is the user's project, so a plantedmcp_server/hooks/<module>.pycould replace the worker.child_commandnow passes-P(Python 3.11+), which keeps the cwd out ofsys.path, for every child it builds: consolidate, capture, session lifecycle and the new prune worker.test_launcher_commandproves it.Refs #711 (does not close it: remaining slices are a size cap with eviction, pruning when a worktree is removed, and a worktree reusing its parent's graph).
Type of change
Test plan
tests_py/hooks/test_session_start_graph_prune.py(5 tests): the scan names only the orphan and deletes nothing (live and unmarked graphs are excluded); one orphan starts exactly one worker with the right module and log; no orphan starts nothing; a failed spawn is logged, not raised; the worker removes the orphan, keeps the live graph and exits 0.pytest tests_py/hooks tests_py/infrastructure/test_code_graph_cache.py tests_py/handlers tests_py/test_ingest_progress.py: 2727 passed, 113 skipped, 2 failed. The 2 failures (test_entry_auto_dispatch.py::test_engine_selected_auto_backend_reaches_hook[False-*]) fail the same way onorigin/main(6a49d69) in this environment.pytest tests_py/scripts: 1851 passed after the worker got itswire_composition_root()call (caught bytest_composition_root_wiring_coverage.py).python scripts/check_craftsmanship.py: OK.ruff checkandruff format --check(0.17.3, fromuv.lock) on the changed files: clean.HOME=$(mktemp -d) python -m mcp_server.hooks.code_graph_prune_backgroundprintedremoved=0 unattributed=0 failed=0, exit 0.Audit notes
Coding-standards compliance
Generated by Claude Code