Repository navigation
Conversation
…trailhq#582) ensureGitignored only read the root .gitignore, so an ignore rule that lives in .git/info/exclude, a parent .gitignore or core.excludesFile was invisible to it and the entry was re-appended on every build, dirtying a checkout whose owner keeps tooling noise out of the tracked .gitignore on purpose. Before appending, ask git: `git check-ignore -q -- <dir>/`. Exit 0 means git already ignores the dir, so return without writing. Any other outcome (not a work tree, git missing, timeout) falls through to the old behaviour. The guard sits after the existing presence check, so a legacy unanchored entry is still handled by whatever precedes it. Tests cover info/exclude, core.excludesFile, a parent .gitignore, and a git repo with no matching rule (still writes). GRAFT_NO_GITIGNORE is unchanged. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Follow-up to the git check-ignore guard in ensureGitignored, from a security review of it. - Run git with GIT_DIR, GIT_WORK_TREE, GIT_INDEX_FILE, GIT_COMMON_DIR, GIT_OBJECT_DIRECTORY and GIT_NAMESPACE removed from its environment. A git hook exports GIT_DIR and GIT_INDEX_FILE, and with them check-ignore answered for another repo, so the entry was skipped in the one being built. - Pass `-c core.fsmonitor=false`, so the call never runs a command named by the repo's own config. - Make the ensureGitignored tests independent of the developer's environment: empty global git config, no default excludes file, GIT_CONFIG_NOSYSTEM, GIT_CEILING_DIRECTORIES above the temp root, and GRAFT_NO_GITIGNORE and GRAFT_NO_IGNORE unset. A global excludes file, a TMPDIR inside a repo or an exported GRAFT_NO_GITIGNORE used to fail them. New test for an inherited GIT_DIR. - Correct the helper's comment about the trailing slash: it makes dir-only patterns match, but negation rules can judge a not-yet-created path differently, so callers create the dir first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
🌱 graft blast radius1 area changed → 3 areas can be affected. 6 dependent symbols, depth 2. flowchart TB
A0(("Graph Building<br/>3 symbols"))
A1(("CLI Entry Point<br/>2 symbols"))
A2(("Pull Request Review<br/>1 symbol"))
classDef reached fill:#D9EDF3,stroke:#3AA7C9,stroke-width:1.5px,color:#0E313C;
class A0,A1,A2 reached;
Who knows this code — 3 people across 4 areas
Ownership is git history over each area's own files, weighted towards recent work (120-day half-life). Merge commits and bots are dropped, and you are dropped from your own PR. A name with no All 6 dependent symbols, grouped by areaGraph Building — 3 symbols in 3 files
CLI Entry Point — 2 symbols in 2 files
Pull Request Review — 1 symbol in 1 file
Test signal per changed area — 1 ✓Reached = a node under a test path has a resolved edge into the changed symbol. It undercounts anything called indirectly — through a CLI, a spawned process or a dynamic import — so read a low ratio as “look here”, never as a coverage gate.
33 test suites also reference this code35 symbols, kept out of the diagram and the table so they cannot crowd out the areas a reviewer has to look at.
Open the interactive graph → — click an area to see its dependent symbols at file:line. |
Fixes #582.
The bug
ensureGitignoreddecides whether the graph dir is ignored by reading the root.gitignoretext. A rule in.git/info/exclude, a parent.gitignoreorcore.excludesFileis invisible to it, so every build re-appendsThe checkout is dirty again after each refresh, in the repos where the owner deliberately keeps tooling noise out of the tracked
.gitignore(an upstream fork is the typical case: the change would ride into a PR branch).GRAFT_NO_GITIGNOREworks around it, but it has to be set on every launcher (MCP server env, each hook, interactive shells; see #514).Repro (main, git 2.x)
The fix
Before appending, ask git:
git check-ignore -q -- <dir>/. Exit 0 means git already ignores the dir, soensureGitignoredreturns without writing. Any other outcome (not a work tree, git not installed, 5 s timeout) falls through to the existing behaviour, so non-git directories and the existing tests are unaffected. The trailing slash makes git treat the path as a directory, so dir-only rules such asgraft/match.The guard sits after the existing "entry already present" check and before the write.
GRAFT_NO_GITIGNOREand the outside-the-root guard are unchanged.Two commits:
fix(gitignore): don't append /graft/ when git already ignores the dir (#582): the guard and its tests.fix(gitignore): harden the check-ignore guard (review follow-ups): the git call runs withoutGIT_DIR,GIT_WORK_TREE,GIT_INDEX_FILE,GIT_COMMON_DIR,GIT_OBJECT_DIRECTORYandGIT_NAMESPACE, with-c core.fsmonitor=false; theensureGitignoredtests no longer depend on the developer's environment; one comment corrected. See "Security review" below.Not in this PR
ensureBuildConfigIgnored(/.graft/, #401) has the same blind spot and can reuse the helper in a follow-up.Merge note: #519
This PR and #519 (legacy-entry migration) both edit the tail of
ensureGitignored, so whichever lands second gets a mechanical conflict insrc/context/node-file.ts. Resolution: keep #519's migration block and itsif (lines.some(entry)) return;, then putif (gitAlreadyIgnores(root, bare)) return;just beforeconst gap. The guard is placed after the presence check so a legacy-migration rewrite still runs first.Design note for review
The guard applies to every caller of
ensureGitignored, including one passingLINK_NOTE(the.graft/read-token dir). No caller insrcdoes that today (LINK_NOTEhas had no caller since b92c22e). If one is added back, it should skip the guard and always write the anchored entry: a rule that exists only in a user-level file or.git/info/excludeprotects that clone and that account only, so another account, a container with a differentHOME, or CI working in the same checkout would not see it, and a read token could be committed. For the regenerable graph cache the guard is the right behaviour; for a token dir it is not. This PR keeps a single behaviour because no token-dir caller exists, and says so here for the maintainers to decide.Tests
test/context.test.ts, five new cases, each in a throwawaygit initrepo:.git/info/excludeignores/graft/: an existing.gitignoreis untouched, and none is created when absent.core.excludesFileignoresgraft/: no.gitignoreis written..gitignoreignoresgraft/and the graph root is a subdirectory: nothing written./graft/is still appended.GIT_DIRpointing at another repo that ignores/graft/does not stop the entry being written (added with the follow-up commit).The file's tests now run with an empty global git config, no default excludes file,
GIT_CONFIG_NOSYSTEM=1,GIT_CEILING_DIRECTORIESset above the temp root, andGRAFT_NO_GITIGNORE/GRAFT_NO_IGNORE/GIT_DIR/GIT_WORK_TREE/GIT_INDEX_FILEunset, restored after each test.node --import tsx --test test/context.test.ts: 38 pass, both withGRAFT_NO_GITIGNORE=1exported and with it unset, and with a hostile environment (globalcore.excludesFileignoringgraft/,TMPDIRinside an ignoring repo,GIT_DIRexported).npx tsc -p tsconfig.json --noEmitis clean. With the source change reverted, tests 1 to 3 fail; with only the follow-up's source change reverted, test 5 fails.Security review
Reviewed by a separate security-review agent before opening this PR. Verdict line, verbatim:
TMPDIRinside a repo,GRAFT_NO_GITIGNORE).GIT_CONFIG_NOSYSTEM,GIT_CEILING_DIRECTORIES,GRAFT_NO_*unset), restored afterwards.GIT_DIR,GIT_WORK_TREEorGIT_INDEX_FILE(for example from a git hook) madecheck-ignoreanswer for another repo, so the entry could be skipped.GIT_COMMON_DIR,GIT_OBJECT_DIRECTORYandGIT_NAMESPACEare removed from the child's environment; new regression test.git check-ignoreruns the repo'score.fsmonitorcommand.-c core.fsmonitor=false. The same exposure in the ingestgit ls-filescalls (src/ingest/fs.ts) is not new and is left for a separate PR.Accepted without change, as the review recommended: when
rootis not a repo but sits inside an outer repo that ignores it, the write is skipped until the next build after agit init, which then self-corrects. Not covered: Windows (baregitresolution againstcwd, the same exposuresrc/ingest/fs.tsalready has).🤖 Generated with Claude Code