Skip to content

Keep repository memory that links outside the workspace out of runs - #2074

Merged
ppXD merged 1 commit into
mainfrom
fix/keep-repository-memory-inside-the-workspace
Oct 6, 2026
Merged

ppXD merged 1 commit into
mainfrom
fix/keep-repository-memory-inside-the-workspace

Conversation

@ppXD

@ppXD ppXD commented Oct 6, 2026

Copy link
Copy Markdown
Owner

Summary

  • The leak. Under the settings pin, Claude 2.1.263 follows a symlinked CLAUDE.md, .claude/CLAUDE.md or .claude directory in an --add-dir wherever it leads. A repository could therefore hand the model a file outside its workspace; on Linux that includes /proc/self/environ, which holds the broker token.

  • The guard. ClaudeWorkspaceMemory resolves everything a directory's memory can reach the way the kernel does (Harnesses/PhysicalPath.File):

    • the memory files and the .claude directory;
    • every markdown file and folder under .claude/rules;
    • every @-import, five hops deep.

    If any of it resolves outside the provisioned workspace, the directory is left out of --add-dir. Rules entries linked outside and outside imports count as escapes too, even though 2.1.263 does not follow them.

  • What still loads.

    • A link that stays inside: CLAUDE.md to AGENTS.md, or a primary-repository cwd's rule pointing into a sibling repository of the same workspace.
    • A link that dangles.
    • Large or wide memory: the bounds (16384 paths, 4 MiB per directory) only cap build cost. A repository with 70 scoped rules, images under rules, a 200 KiB CLAUDE.md or import, or 1100 email addresses keeps its memory.
  • Failure handling. Memory the guard cannot check is left out instead of failing the launch. The workspace root and its memory go through the same walker, so a link whose target climbs .. out of another link no longer throws before the launch.

  • Timeline. SandboxSpec.LaunchNotices (never serialized) becomes one Warning at launch. A revise round adds another only when what it leaves out changes, including a note when the memory loads again.

  • Refactor. CodexHarness.PhysicalDirectory moves unchanged to Harnesses/PhysicalPath.

Test plan

  • Unit: full suite 11848 passed (1 skipped), including:
    • escape and kept shapes, and the bounds on both sides of each limit;
    • a primary-repository cwd, a workspace reached through a .. link, and the notice name cut;
    • the launch-notice change rule.
  • Unit: TempTree roots every tree through a symlink. Comparing the workspace lexically fails 36 cases both with the macOS TMPDIR and with a TMPDIR that has no symlink above it.
  • Integration (Postgres, git 2.56): RealHarnessWorkspaceMemoryTests 4/4.
    • Covers memory unchanged across rounds, and the draft round pointing CLAUDE.md outside and back inside.
    • With the guard disabled, 3/4 fail. With the revise-round notice skipped, 2/4 fail.
  • Integration: the executor, revise-loop and harness subset passed 252/252.
  • E2E on macOS, real Claude 2.1.263: A_claude_run_leaves_out_repository_memory_that_links_outside_the_workspace has a repository each for a linked CLAUDE.md, a linked .claude/CLAUDE.md and a linked .claude directory.
    • Each has a positive control that sees the outside file leak.
    • With the guard disabled, the E2E finds 3 leaks; guarding CLAUDE.md only, it finds 2.
  • CI root sandbox lane: floor 98 → 99, marker [repo-config-e2e] ran memory-link-outside claude-code multi-repo Confined

@ppXD
ppXD force-pushed the fix/pin-which-repository-surfaces-claude-runs-load branch from f2e44f9 to b9613a4 Compare October 6, 2026 16:30
@ppXD
ppXD changed the base branch from fix/pin-which-repository-surfaces-claude-runs-load to main October 6, 2026 16:30
@ppXD
ppXD force-pushed the fix/keep-repository-memory-inside-the-workspace branch from 4fcfe6d to 4057677 Compare October 6, 2026 16:31
The settings pin adds the workspace and each repository in it back with
--add-dir so their memory loads, and the pinned CLI opens an added
directory's CLAUDE.md and .claude/CLAUDE.md by path and follows a
symlink at either, or at .claude itself, wherever it leads: a
repository that commits CLAUDE.md as a link to a file outside its
workspace hands that file to the model as the repository's
instructions. On Linux one such target is /proc/self/environ, which
holds the CLI's environment and with it the run's broker token.

Before a directory is added, everything its memory can reach is now
resolved one component at a time, the way the kernel resolves it: the
memory files, the .claude directory, every markdown file and folder
under .claude/rules, and every file they @-import, five hops deep, with
any '@' run taken for an import. 2.1.263 follows neither a rules entry
linked out nor an import that resolves outside its cwd; the guard
treats both as escapes anyway, for a later CLI that does. If any of it
resolves outside the workspace the directory is left out whole. When
none is left the run carries no --add-dir and no memory switch, and
keeps the settings pin. A link that stays inside the workspace
(CLAUDE.md to AGENTS.md, or a primary-repository cwd's rule into a
sibling repository of the same workspace) or one that dangles still
loads. The workspace is resolved by the same walker as what its memory
reaches, so a link whose target climbs out of another link cannot make
the two disagree, or throw before the launch. Files are opened
no-follow, non-blocking and only if regular, so a FIFO cannot hang the
build.

The bounds only cap what one build spends: 16384 paths resolved and
4 MiB read per directory. What cannot be checked within them, or at
all, leaves the directory out instead of failing the launch. A
repository with 70 scoped rules, rules beside images, a 200 KiB
CLAUDE.md or import, or a thousand email addresses keeps its memory.

The harness reports what it left out on SandboxSpec.LaunchNotices,
which never reaches the serialized spec. The executor records them as
one Warning event when the run launches, and again for a revise round
only when they differ from what the run last said, since a round's
agent can re-point the memory either way.

CodexHarness's physical-directory resolver moves unchanged to a shared
PhysicalPath helper. The Claude harness unit tests stop naming a
literal /tmp/ws, which the harness now reads, and lay their trees out
under a symlinked root, so a lexical workspace comparison fails on
every host and not only on macOS.

Against Claude 2.1.263 the E2E's positive control, the same workspace
with the left-out repositories put back into --add-dir, hands the
outside file behind a linked CLAUDE.md, a linked .claude/CLAUDE.md and
a linked .claude directory to the model; the guarded run hands none.
Disabling the guard fails that E2E, the integration test and every
escape case.
@ppXD
ppXD merged commit 56af7fb into main Oct 6, 2026
@ppXD
ppXD deleted the fix/keep-repository-memory-inside-the-workspace branch October 6, 2026 16:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant