A labelled source grants everything in it, including the files git was told to ignore. Verified against main @ 6dbf1c6.
What ships
grep -rni "gitignore" packages/cli/src apps/relay/src
returns nothing. classifyPath resolves a label by longest-prefix over sources, guard.ts subtracts eight basename regexes (.env, .env.* minus example/sample/template, id_rsa, id_ed25519, id_ecdsa, .pem, .p12, .pfx), and nothing else is excluded. A build artefact, a *.local override, a scratch dump, a .terraform/ state directory or a downloaded fixture inside a labelled repo is fully readable by a cleared caller.
defaultSensitivityMap labels the enclosing git repository internal on every new line (commands/line.ts:157), so this applies to the default install, not just to hand-labelled sources.
Why this is the gap worth closing first
From docs/research/2026-08-06-repo-seed-default-evidence.md: every comparable product auto-grants broadly and ships a default exclusion list. We do the first half only.
| Product |
Default scope |
Default exclusions |
| Cursor |
indexes the workspace |
"Cursor automatically ignores files in .gitignore" plus a built-in list incl. .env*, lockfiles, node_modules/, __pycache__/, binaries (docs) |
Copilot @workspace |
indexes project text files, "not limited to specific file types" |
.gitignore, files.exclude, common irrelevant types (docs) |
| Claude Code |
files in the launch directory, no prompt |
none documented |
| AgentCall |
the enclosing git repo, internal |
8 basename regexes |
The grant is not the deviation — Claude Code, Cursor, Copilot and Cody all auto-grant the working context. The exclusion list is the deviation, and it is the cheapest thing to fix.
It also targets the right files. .gitignore is, almost by construction, the owner's own statement that a file is machine-local and not part of the shared artifact — which correlates with build output, local credentials, and *.local config, and anti-correlates with anything a colleague would call to ask about. Nobody has to maintain a new list; the signal already exists in the repo.
What it does not fix
Roughly 87–95% of the residual. Two independent measured distributions put basename-style coverage at 13.3% (GitGuardian extension table, 2020 data) and ~5.4% (SecretBench, MSR 2023, 15,084 manually-labelled true secrets). A key hardcoded in config.py is git-tracked and stays readable — that is #173's territory, and #173 is a detector rather than a boundary (Presidio at 0.07 recall on HIGH-sensitivity categories, arXiv 2606.19881).
So this is not a fix for the seed decision in #393. It is the one uncontroversial subtraction available while that decision is open.
Cursor's own caveat is the right calibration for how much it buys: "While Cursor blocks ignored files, complete protection isn't guaranteed due to LLM unpredictability."
Design questions before coding
- Which ignore semantics. Full gitignore precedence is non-trivial — nested
.gitignore files, negation patterns, .git/info/exclude, the global core.excludesFile. A dependency is likely cheaper and more correct than hand-rolling, but it lands in guard-entry.ts's import graph, which is pinned to exactly zod by guard-entry import budget in test/guard-entry.test.ts and carries a measured per-tool-call cost (see 2026-08-06-guard-entry-import-cost.md). This is the main cost and it should be measured, not assumed.
- Where it sits in the lattice. Cleanest is probably a
secret-equivalent exclusion evaluated alongside DENIED_BASENAMES rather than a new source rule, so longest-prefix-wins cannot override it — same construction as builtinSecretSources. Should an owner be able to override it? Argument for no: an ignored file is machine-local by the owner's own declaration.
Grep/Glob roots and selectors, not just Read targets — a scan whose root is permitted must not return ignored files either. SCANNING_ROOT and SELECTOR_KEY both need to see this.
- Cost per tool call. Reading and compiling ignore rules on every
PreToolUse is not obviously acceptable; caching is per-process and the guard is one process per tool call.
- What happens outside a git repo, where there is no
.gitignore to read.
Related
A labelled source grants everything in it, including the files git was told to ignore. Verified against
main@6dbf1c6.What ships
returns nothing.
classifyPathresolves a label by longest-prefix oversources,guard.tssubtracts eight basename regexes (.env,.env.*minusexample/sample/template,id_rsa,id_ed25519,id_ecdsa,.pem,.p12,.pfx), and nothing else is excluded. A build artefact, a*.localoverride, a scratch dump, a.terraform/state directory or a downloaded fixture inside a labelled repo is fully readable by a cleared caller.defaultSensitivityMaplabels the enclosing git repositoryinternalon every new line (commands/line.ts:157), so this applies to the default install, not just to hand-labelled sources.Why this is the gap worth closing first
From
docs/research/2026-08-06-repo-seed-default-evidence.md: every comparable product auto-grants broadly and ships a default exclusion list. We do the first half only..gitignore" plus a built-in list incl..env*, lockfiles,node_modules/,__pycache__/, binaries (docs)@workspace.gitignore,files.exclude, common irrelevant types (docs)internalThe grant is not the deviation — Claude Code, Cursor, Copilot and Cody all auto-grant the working context. The exclusion list is the deviation, and it is the cheapest thing to fix.
It also targets the right files.
.gitignoreis, almost by construction, the owner's own statement that a file is machine-local and not part of the shared artifact — which correlates with build output, local credentials, and*.localconfig, and anti-correlates with anything a colleague would call to ask about. Nobody has to maintain a new list; the signal already exists in the repo.What it does not fix
Roughly 87–95% of the residual. Two independent measured distributions put basename-style coverage at 13.3% (GitGuardian extension table, 2020 data) and ~5.4% (SecretBench, MSR 2023, 15,084 manually-labelled true secrets). A key hardcoded in
config.pyis git-tracked and stays readable — that is #173's territory, and #173 is a detector rather than a boundary (Presidio at 0.07 recall on HIGH-sensitivity categories, arXiv 2606.19881).So this is not a fix for the seed decision in #393. It is the one uncontroversial subtraction available while that decision is open.
Cursor's own caveat is the right calibration for how much it buys: "While Cursor blocks ignored files, complete protection isn't guaranteed due to LLM unpredictability."
Design questions before coding
.gitignorefiles, negation patterns,.git/info/exclude, the global core.excludesFile. A dependency is likely cheaper and more correct than hand-rolling, but it lands inguard-entry.ts's import graph, which is pinned to exactly zod byguard-entry import budgetintest/guard-entry.test.tsand carries a measured per-tool-call cost (see2026-08-06-guard-entry-import-cost.md). This is the main cost and it should be measured, not assumed.secret-equivalent exclusion evaluated alongsideDENIED_BASENAMESrather than a new source rule, so longest-prefix-wins cannot override it — same construction asbuiltinSecretSources. Should an owner be able to override it? Argument for no: an ignored file is machine-local by the owner's own declaration.Grep/Globroots and selectors, not justReadtargets — a scan whose root is permitted must not return ignored files either.SCANNING_ROOTandSELECTOR_KEYboth need to see this.PreToolUseis not obviously acceptable; caching is per-process and the guard is one process per tool call..gitignoreto read.Related
internal— measurably the wrong default #393 — the seed decision this does not resolve, and the research that surfaced itggshieldplugs into the hook seam we already own, but it is fail-open where our guard is fail-closed #173 — content scanning; the acknowledged residual