SIGN IN SIGN UP

fix(proxy): scope repo intelligence to project source and require direct evidence (#901)

* fix(proxy): skip vendor trees in repo intel

BM25 was indexing .pixi/envs and other dependency
dirs, then injecting those file:line guesses every
turn. Exclude known vendor trees and drop the path
block when no local scout is configured.

Closes #899

Co-authored-by: Julius Brussee <JuliusBrussee@users.noreply.github.com>

* fix(proxy): detect dep trees by content, not name

Name denylist missed conda envs, undotted pixi, and
virtualenvs under any name, while dropping real
first-party build/ and vendor/ dirs.

- listFiles prefers `git ls-files --exclude-standard`,
  so the project's own .gitignore defines vendored
- walk fallback identifies environments by contents
  (conda-meta, pyvenv.cfg, site-packages child),
  catching the interpreter stdlib they carry
- ambiguous build/dist/target/vendor need a sibling
  manifest before exclusion
- symlink and junction dirs are never followed

Refs #899

Co-authored-by: Julius Brussee <JuliusBrussee@users.noreply.github.com>

* fix(proxy): treat the indexed repo as untrusted input

Second review found the git listing both unsafe and
incomplete.

- gitCommand pins core.fsmonitor and other exec
  vectors off and strips GIT_*; fsmonitor runs during
  an index read, so `ls-files` in a hostile clone ran
  attacker code at session start
- git-listed paths now run the same content filter as
  the walk, so an un-ignored conda prefix or committed
  vendor tree no longer rides in on git's say-so
- an empty-but-successful listing means the cwd is
  ignored by an enclosing repo; fall back to the walk
  instead of reporting a complete map
- Direct matches the file name, not the whole path:
  a term like "src" marked every file beneath it
- ListingBasis discloses which path ran, hashed in

Refs #899

Co-authored-by: Julius Brussee <JuliusBrussee@users.noreply.github.com>

* fix(proxy): log declined evidence, document invariants

A declined turn stays silent to the model but now
records repository_evidence_declined_no_direct_match,
so weak evidence is distinguishable from a broken or
disabled mechanism.

Adds a positive control to the suppression test and
records the gating, filtering, and git-hardening
invariants in proxy/CLAUDE.md.

Refs #899

Co-authored-by: Julius Brussee <JuliusBrussee@users.noreply.github.com>

* fix(installer): make prune path flavour explicit, not host-derived

pruneOrphanedManagedHooks guards against pruning a hook whose path is
absolute on the other platform. That guard is POSIX-only by construction:
win32's absolute set is a superset of posix's, so a Windows host reads
C:\... natively and prunes it. The test asserting the guard hard-coded a
Windows path and so failed on the windows CI runner.

Select the path flavour from an explicit platform arg (default
process.platform, same as rewriteLegacyManagedHookCommands) so the POSIX
side stays reachable from a Windows runner. Host behaviour unchanged.

Co-authored-by: Julius Brussee <JuliusBrussee@users.noreply.github.com>

* test(cli): escape stub preload path in NODE_OPTIONS

Node parses NODE_OPTIONS itself and treats a backslash inside a quoted
value as an escape, so the raw Windows path reached --require with every
separator eaten:

  Cannot find module 'C:UsersRUNNER~1AppDataLocalTempcave-pi-enableXbinstub-dispatch.cjs'

Every stub-backed process died in the preload, failing all six pi enable
and wrap round trips. The quotes must stay for profile names with spaces,
so double the separators instead. POSIX is unaffected — stubEnv returns
before this on non-Windows.

Never caught before because npm test failed earlier in the same job, so
test:windows has not run since these tests joined the gate.

Co-authored-by: Julius Brussee <JuliusBrussee@users.noreply.github.com>

* fix(proxy): harden every untrusted git call, not just repo intelligence

`repointel` hardened its own `git ls-files` and left three other call sites
raw. `nativehook.currentRepositoryState` runs `git status` on EVERY hook
event, and `status` refreshes the index, so it triggers `core.fsmonitor`
harder than `ls-files` does — verified: the sentinel hook fires. Same for
`store/detect_outcomes` and the two `execFileSync` sites in the CLI.

- `proxy/internal/gitsafe` and `packages/cli/src/git-safe.ts` now own the
  `-c` override list; all five call sites route through them.
- `verify_repo.py` fails the build on a NEW raw `git -C <dir>` call site,
  which is the regression class this bug actually belongs to.
- `Bundle.DirectOnly()` strips metadata-only items BEFORE the evidence
  object is written. Filtering only at render time left the BM25-proximity
  guesses stored behind the `ccr://` handle, one retrieval from the model,
  which is what the block refuses to render.
- `gitListBudget` caps git at half the caller's remaining warm budget. 2s
  of a 3s window left the fallback walk — the MORE expensive path — unable
  to finish, so the map failed closed in exactly the case the fallback
  exists for.
- `NormalizeTerms` floors terms at 3 characters, matching the hook that
  produces them: direct evidence is a substring test on a file's own name,
  so `go`/`py`/`js` marked every file of that language a direct hit.

Regression tests verified to fail without their fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(build): stop wedge generation racing itself on Windows

mcp, shrink, and browse each run gen-wedge-installer from their own
prepack/pretest, and every run writes all four targets, not just its own.
`pnpm -r test` runs those packages in parallel, so several processes
copy onto the same file at once and Windows fails the loser:

  EBUSY: resource busy or locked, copyfile
  packages/shared/binary-installer/installer.mjs
  -> mcp/bin/binary-installer.generated.mjs

The bytes are identical every time and the outputs are committed, so the
race was over writes that change nothing. Skip when the destination
already matches, and land a real change through a temp file + rename so
no concurrent reader sees a partial file. Verified with a 12-way
concurrent storm, and against corrupted outputs to prove the write path
still restores them.

Co-authored-by: Julius Brussee <JuliusBrussee@users.noreply.github.com>

* fix(proxy): keep system git config readable while hardening

GIT_CONFIG_NOSYSTEM defends against nothing here — /etc/gitconfig is
root-owned, the threat is the repository's own .git/config, and the `-c`
overrides beat every config level anyway. What it does cost is org-wide
`safe.directory` allowances: without them `git status` fails "dubious
ownership" on shared and CI machines, repository state comes back empty,
and reuse silently stops. Dropped from both wrappers, with a test so it
does not come back.

Also names the fsmonitor tradeoff at the hot call site: disabling it taxes
the people who configured it because their repo is huge, and status has a
100ms budget. That degrades safely; moving the budget is a latency
decision for every user and needs a measurement this fix does not have.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* test(build): pin wedge generation against the concurrency it now survives

The EBUSY fix has no committed regression proof, and the failure only
appears when several packages run the script at once — which is exactly
what `pnpm -r test` does and what a single-process check never reproduces.
Eight concurrent runs must all succeed and leave the shared outputs
byte-identical; a drifted output must still be repaired, so skip-if-
identical cannot regress into skip-always.

Runs in root `npm test`, which is the gate the Windows job failed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Julius Brussee <JuliusBrussee@users.noreply.github.com>
Co-authored-by: Julius Brussee <juliusbrussee@getlockedin.nl>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
J
Julius Brussee committed
1b7ff8fae926c6fe3b72ad3786b14ebc11889d52
Parent: 7bb7130
Committed by GitHub <noreply@github.com> on 8/24/2026, 11:12:21 PM