Skip to content

A labelled source grants .gitignore'd files too — every comparable product excludes them #397

Description

@KenTaniguchi-R

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. What happens outside a git repo, where there is no .gitignore to read.

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions