Skip to content

chore: remove local machine paths from the repo and guard against new ones - #1051

Merged
blafourcade merged 11 commits into
nextfrom
chore/no-local-paths-in-the-repo
Oct 17, 2026
Merged

blafourcade merged 11 commits into
nextfrom
chore/no-local-paths-in-the-repo

Conversation

@blafourcade

@blafourcade blafourcade commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

🎯 What & why

Task documents and their captured evidence held paths and identifiers from one contributor's machine: home directories, Claude scratchpad paths, macOS temp folders, worktree layout, session ids and a device id. Two kanban specs held a third party's personal email address. This replaces them with placeholders in 164 files and adds a check so none comes back.

🛠️ How it works

  • Cleanup (adfbfc4, fe85acb, 10dbf9f, 6059541): files under aidd_docs/tasks/, cli/aidd_docs/tasks/ and two kanban/aidd_docs/tasks/ specs. Each value is now a placeholder: <home>, <worktree>, <scratch>, <tmp>, <session>, <device>, <installation>, <user> (owner column of an ls -l capture).
    • Commands in fences were rewritten so they still read as commands: a relative path (cd cli, cd ../test-sdlc-891-codex) or a quoted placeholder (node "<worktree>/cli/dist/cli.js", B='<scratch>/bench').
    • Session ids were replaced where a key or folder names them (session_id, sessionId, session, Codex session id:, Copilot session-state/<id>, Codex rollout-<time>-<id>.jsonl file names, whose timestamps are replaced too because they give away the timezone), along with the other places the same ids appear (thread_id, prompt_cache_key, window ids, copilot --resume=). The device_id fingerprint and Codex installation_id that sit next to them were replaced too. The seven Codex threadIds in the sandbox observations are <session-1> to <session-7>, so uniqueThreadIds: 7 still holds. Random per-run ids stay: context_window_id, turn_id, root_turn_id and msg_ ids. Made-up ids (3333…, 0000…000a) are unchanged.
    • cli/.claude/scheduled_tasks.lock, already listed in cli/.gitignore, was tracked with a live session id; it is now untracked.
    • Two kanban specs named a third party's personal email address as Decider/Owner/Consumer; it is now someone@example.com.
    • Test fixtures are untouched: their placeholder accounts (/home/user, C:\Users\someone, …) stay.
  • Guard (scripts/check-no-local-paths.js): it fails on a home path, raw or JSON-escaped (/Users/<name>, /home/<name>, C:\Users\<name>, with or without a trailing separator), a home folded into a project slug (-Users-<name>-, C--Users-<name>-), /private/tmp/claude-<uid>/, /var/folders/, and a UUID that a session or thread key (session, session_id, sessionId, thread_id, threadId, threadIds, including every id of an array under one) or Copilot's session-state/ folder names, and a UUID naming a .jsonl transcript (<project>/<id>.jsonl, D:\work\<id>.jsonl, rollout-<time>-<id>.jsonl). Each hit is reported as file:line: <pattern>; the matched text is never printed.
    • It matches patterns, never the current account, so it works for any contributor.
    • A name only counts at the root of a path and must start with an alphanumeric. /proj/home/x/ and the fake HOME /home/.claude/ therefore pass. A session UUID with fewer than 8 distinct hex digits is treated as made up (00000000-…-00000000000a) and passes.
    • Allowlist: user, u, a, me, dev, someone, alice, tester, test, p, aidd, jane, first, runner, runneradmin, compared case-insensitively, ignoring a ~N 8.3 suffix and a trailing .. Each one is matched in a test or task doc on the tree; removing any one turns --all red. jane/first are the first word of Jane Doe/First Last; runner/runneradmin are the GitHub Actions accounts. No path is excluded; binary files are skipped.
    • Files are read inside a try and skipped on ENOENT/EISDIR, with no existence check first (CodeQL js/file-system-race).
    • Wiring: lefthook pre-commit → no-local-paths on {staged_files}. In CI, validate.yml already runs lefthook run pre-commit --all-files, so it checks every tracked file. --all runs the same scan by hand.
    • {staged_files}, not {files}: with no glob, {files} was skipped with "no files for inspection" on a staged leak.

🧪 How to verify

  • Red, the check run on an origin/next checkout before the cleanup:
    $ node scripts/check-no-local-paths.js --all   # cwd: detached worktree of origin/next
    ❌ 1883 local machine path(s) in tracked files
      aidd_docs/tasks/2026_07/2026_07_08_onboard-tutorial-rebuild/headless-runs-codex.txt:15: /private/tmp/claude-<uid>/
      aidd_docs/tasks/2026_07/2026_07_08_onboard-tutorial-rebuild/headless-runs-codex.txt:30: /Users/<name>/
    ...
    
    By pattern: 1034 /var/folders/, 743 /Users/<name>/, 106 /private/tmp/claude-<uid>/, across 110 files.
  • Green on this branch: node scripts/check-no-local-paths.js --all → ✅ No local paths: 0 leak(s) in 3154 file(s).
  • The final guard on origin/next (3167d690): 2179 hits (1034 /var/folders/, 743 /Users/<name>, 188 session id, 106 -Users-<name>-, 106 /private/tmp/claude-<uid>/, 2 session file). On this branch: 0. On the merge of this branch with origin/next (git merge-tree, exit 0): 0 leak(s) in 3173 file(s).
  • Mutation: restore line 54 of aidd_docs/tasks/2026_10/2026_10_15_ledger-by-day/phase-5.md from origin/next →
    phase-5.md:54: -Users-<name>- and phase-5.md:54: /private/tmp/claude-<uid>/, exit 1.
  • Tests first: scripts/tests/no-local-paths.test.js was red (Cannot find module), then green; red again on each new shape before its pattern landed. Mutants run against the test file, each red: each new pattern (slug, session key, transcript name, its rollout- and / prefixes), the bare-name lookahead, the trailing-. strip, the ENOENT skip, the made-up-id threshold at < 16 and at < 5, the escaped-key support removed, threadId alone removed, thread_id/threadIds removed, the array pass removed, an array read to the last ] instead of the next, the line sort removed, the skip past an array removed, the line search off by one, < offset - 2 and high = newlines.length - 1 in the line search, a stale ] reused for a later array, return turned into continue on an unclosed array (pinned by a count of ] scans, not a time bound), the array cap at 0, .jsonl turned into .json or .jsonl?, and the backslash branch of the transcript pattern removed. Allowlist names are pinned by --all on the tree, not by the unit tests: removing any one turns it red.
  • Cost of the array pass on synthetic input, round-3 code → now (findLocalPaths, one run each): 480 KB of unclosed "threadIds": [ 2127 → 19 ms; 20000 single-id arrays 2588 → 40 ms; 100k ids in 2000 arrays 70312 → 181 ms. --all on the tree: 0.42-0.43 s before, 0.47-0.49 s now. The pass looks up each array's ] once, and stops when no ] is left in the text.
  • Gates: node --test 'scripts/__tests__/**/*.test.js' 474 pass, 0 fail (13 of them in the guard's own test file). lefthook run pre-commit --all-files --force is all green. lefthook run pre-push is green: cli-knip, and cli-test with 6748 passing.
  • git diff origin/next...HEAD: no added line holds the username, /var/folders/, /tmp/claude-<n> or the worktree layout.

⚠️ Heads-up

  • Git history still holds every old path and id. Rewriting history was deliberately not done. This PR cleans the working tree and stops new leaks.
  • Some evidence manifests hash capture files whose content was redacted: rule-contracts/evidence/{index,copilot/index,cursor-native/index}.json, autonomous-rule-generation/rule-presentation/evidence/index.json and realistic-sandbox/native-evidence/index.json. Those hashes still describe the original captures, which only history holds. No test verifies them.
  • /private/tmp/aidd-* sandbox paths remain on purpose: they name no account.
  • Untracking cli/.claude/scheduled_tasks.lock deletes it from other checkouts on their next pull. It is a short-lived scheduler lock, recreated on demand.
  • Not covered by the guard, one line each:
    • an array under a session key that is nested, or whose closing bracket is missing;
    • an id more than 64 KB into one array, and a session array that starts more than 64 KB after an unclosed [ (the unclosed [ takes the next ] as its own, and only its first 64 KB is read);
    • a bare username outside a path (an ls -l owner column, whoami or $USER output);
    • a session id under any other key, or not shaped as a UUID;
    • a session id under an unquoted YAML or JS key (session_id: <uuid>);
    • a UUID in a file name other than a .jsonl transcript, and a bare <uuid>.jsonl with no folder before it;
    • a session id passed as a CLI flag (--resume <uuid>);
    • email addresses;
    • Claude stream "uuid" message ids.

🔗 Linked issue

None. Found by the R1 telemetry cleanup audit.

✅ I certify

  • I DO CERTIFY I READ EACH LINE OF THE PULL REQUEST BECAUSE I AM A SOFTWARE ENGINEER, NOT A AI PUPPY.

🤖 Generated with Claude Code

https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp

blafourcade and others added 3 commits October 17, 2026 14:43
Task documents and their captured evidence carried absolute home paths,
Claude scratchpad paths, macOS temp folders, worktree layout, session
ids and a device id from one contributor's machine. Each is now a
neutral placeholder: <home>, <worktree>, <scratch>, <tmp>, <session>,
<device>, <installation>, <user>. Runnable commands use a relative path
or a quoted placeholder. Git history still holds the old values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
A home directory, a Claude scratchpad or a macOS temp folder names its
owner and means nothing elsewhere. The check matches patterns, never the
current account, so it works for every contributor; a short allowlist
keeps the placeholder accounts tests already use. Pre-commit checks the
staged files; validate.yml runs the same hook with --all-files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
runneradmin never matched the guard on the tree: every occurrence ends
without a separator. The allowlist keeps only names the check meets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
Comment thread scripts/check-no-local-paths.js Fixed
blafourcade and others added 8 commits October 17, 2026 15:07
Session ids under a plain "session" key and in Copilot's session-state
folder are now <session>, wherever the same id appears. The tracked
cli/.claude/scheduled_tasks.lock, already gitignored, held a live
session id and is untracked. A third party's personal address in two
kanban specs becomes someone@example.com.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
A home no longer needs a trailing separator, so a bare $HOME value is
caught. A home folded into a Claude project slug, and a UUID a session
key or Copilot's session-state folder names, are caught too; made-up
ids that repeat a few digits pass. The allowlist grows only by the
placeholder accounts the tree's tests use. Files are read inside a
try, without a racy existence check first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
Both were real UUIDv7 session ids in rollout file names, and the file
name's local time, read against the id's own clock, gave away the
contributor's timezone. Both become rollout-<timestamp>-<session>.jsonl.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
A UUID naming a .jsonl transcript, Claude's <project>/<id>.jsonl or
Codex's rollout-<time>-<id>.jsonl, is now a leak, with the same
made-up-id skip as a session key. Tests pin the made-up-id boundary
(seven distinct digits pass, eight fail), a thirteen-digit real-looking
id, and a JSON-escaped session key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
The threadIds list held the real ids of seven Codex runs. They become
<session-1> to <session-7>, so uniqueThreadIds: 7 still reads true.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
thread_id, threadId and threadIds join the session keys, and an array
under any of them is read whole, each id reported on its own line. A
Windows transcript path is covered by a test, and a .json named by a
UUID pins the pattern to .jsonl only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
Each array under a session key is found by its opening bracket, and the
next closing bracket is looked up once and shared by every key before
it, so an unclosed key no longer rescans the text. A body is read up to
64 KiB, and a line number comes from a binary search over newline
offsets computed once. Tests pin threadId, where an array ends, the
line order of mixed hits, and a key nested in an array read once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
The previous commit said the next closing bracket was shared by every
key before it. It never was: the search always resumes past that
bracket, so the keys inside an array are skipped, not visited. The
lookup is now made once per array, and the first array with no closing
bracket ends the pass. Tests pin two arrays in one text, an unindented
id ending the text, and a single bracket scan for many unclosed keys.
The 64 KiB body cap is documented as an assumption.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
@blafourcade
blafourcade marked this pull request as ready for review October 17, 2026 16:09
@blafourcade
blafourcade requested a review from a team as a code owner October 17, 2026 16:09
@blafourcade
blafourcade merged commit 5721e2f into next Oct 17, 2026
61 checks passed
@blafourcade
blafourcade deleted the chore/no-local-paths-in-the-repo branch October 17, 2026 16:09
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.

2 participants