Skip to content

fix(tunnel): keep tunnel captures out of git - #952

Open
tolgaergin wants to merge 4 commits into
mainfrom
fix/tunnel-capture-gitignore
Open

tolgaergin wants to merge 4 commits into
mainfrom
fix/tunnel-capture-gitignore

Conversation

@tolgaergin

@tolgaergin tolgaergin commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Keeps tunnel captures out of git automatically. Until now the CLI only told users to ignore them, and that advice is easy to miss.

.lpm/inspector.db holds every captured request's and response's full headers and bodies, including Authorization, Cookie and *-Signature headers. The CLI warned users to add .lpm/inspector.db* to .gitignore. That warning had three gaps:

  • --json and lpm dev --quiet hid it;
  • it came after the database already existed;
  • a .lpm/inspector.db* rule in a repository's root .gitignore is anchored to the root, so it misses a tunnel started in a subdirectory.

One project committed its capture this way and kept it tracked for six months.

Changes

Capture files (crates/lpm-inspect/src/db.rs)

  • InspectorDb::open prepares the capture files before SQLite opens them:
    • It refuses a linked .lpm directory, or a linked or non-file inspector.db, -wal, -shm or -journal, since a cloned repository could point them anywhere.
    • It makes existing capture files owner-only, as a checkout or an older release may have left them readable.
    • It creates a new database owner-only, so it's never readable by others.
    • SQLite opens the database with SQLITE_OPEN_NOFOLLOW, on the directory's real path.
  • It writes .lpm/.gitignore next: it adds inspector.db*, which also covers the -wal, -shm and -journal files, unless git would already read the file as ignoring them.
    • Lines are read as git reads them: only a trailing CR and unescaped trailing spaces are dropped. /inspector.db* counts, and a later ! rule for the database needs the rule added again.
    • The file is read without following links, up to the shared 16 MiB config cap, and written through a temporary file and a rename. It keeps the file's own line endings.
    • A linked or read-only .lpm/.gitignore is left as it is and logged. The warning then says git doesn't ignore the captures.
    • A file inside .lpm/ holds wherever the project sits in a repository, and leaves the project's own .gitignore alone.
    • Every caller that opens the database goes through it: lpm tunnel, lpm dev --tunnel, and inspect, log and replay.

The capture warning (crates/lpm-cli/src/commands/tunnel.rs)

  • It asks git how it treats the capture files: git ls-files -z -t --cached --others --exclude-standard -- ':(glob,icase).lpm/inspector.db*'.
    • It says .lpm/.gitignore keeps the database out of git only when git ignores every capture file.
    • It names files git tracks. An ignore rule can't stop those from being committed, so it gives the command to stop tracking them, git rm --cached -- '.lpm/inspector.db', and says to commit and to rotate any credentials they held.
    • It names files git neither tracks nor ignores.
    • Names are quoted, and at most five are listed. Past five, the command uses the capture pathspec instead of every name.
    • Names match without regard to case, as on a case-insensitive disk.
    • Outside a repository, without git, or when git doesn't answer within 2 s, the warning says nothing more. The check runs async, so a slow repository can't stall the tunnel's start.
  • Git runs no repository code. hardened_git_command (npmrc.rs) passes -c core.fsmonitor=false -c safe.bareRepository=explicit, and removes GIT_* variables. It's now pub(crate), and lpm setup local gets the same flags.
  • The warning prints once.
    • The CLI's default log filter lpm=warn matches lpm_cli::tunnel by prefix, so the log copy printed alongside the human one.
    • Now the human warning shows, or the log line when the human one is hidden (--json, --quiet).
  • A doc comment in install/gitignore.rs named two helpers that no longer exist.

--no-inspect still saves captures. The docs say so ("disables the browser UI while capture continues"), and inspect/log/replay read them, so it's unchanged.

Testing

  • lpm-inspect:
    • a new database's .lpm/.gitignore, written once across reopens;
    • an existing file gains the rule after its own lines, with its own line endings;
    • lines git reads as the rule are left as they are, and lines it doesn't read as the rule (leading whitespace, a tab, NBSP, a later negation) get it added;
    • a read-only file is left alone;
    • on Unix:
      • a linked .lpm/.gitignore and its target are left alone;
      • a linked .lpm is refused;
      • a linked database or -wal file is refused, and its existing outside target keeps its mode and content;
      • leftover -wal, -shm and -journal files are made owner-only before SQLite opens them.
  • lpm-cli, in fresh repositories:
    • tracked capture files are named, in any letter case, and .lpm/added-sources.json isn't;
    • files git neither tracks nor ignores are told apart from tracked ones;
    • nothing is named outside a repository or for an ignored database;
    • the repository's core.fsmonitor program doesn't run;
    • a carried bare repository is refused;
    • names are quoted for a shell and capped at five;
    • the untrack command switches to the pathspec past five.
  • Workflow tests (dev_tunnel):
    • the warning prints exactly once;
    • a tracked capture is named with its git rm command and no "kept out" claim;
    • the claim appears once git ignores the files.
  • Mutations: 14 were run against the new code. All but one are caught. The exception is dropping safe.bareRepository=explicit, which ls-files refuses in a bare repository anyway; the flag is defense in depth.

The CI gate passes locally on 2edf4d1, with every workspace member rebuilt from this branch:

  • cargo clippy --workspace --all-targets --locked -- -D warnings, cargo fmt --check and cargo build --workspace --locked;
  • 7,158 workspace tests;
  • cargo test -p lpm-cli --bin lpm-rs -- --test-threads=1: 5,411 passed;
  • 119 binary-surface tests;
  • the run and cache workflow tests (172), and the dev and dev_tunnel workflow tests (103), with the hermetic CLI.

Docs: lpm-dev/rust-client-docs#391.

🤖 Generated with Claude Code

Opening the capture database first adds `inspector.db*` to
`.lpm/.gitignore`, so captures, which hold request headers and bodies,
stay out of git wherever the project sits in a repository, without
editing the project's own `.gitignore`. A linked `.lpm/.gitignore` is
left alone. The capture warning names files git already tracks, which an
ignore rule can't stop from being committed, and prints once: as a human
warning, or on the log when that's hidden.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
tolgaergin and others added 3 commits October 11, 2026 20:26
…bout captures

The git call uses `-c core.fsmonitor=false -c safe.bareRepository=explicit`,
so neither an archived project's fsmonitor nor a bare repository a
cloned project carries can run code, and runs async with a 2 s timeout.
The capture warning asks git which capture files it tracks or doesn't
ignore, case-insensitively, says captures are kept out of git only when
git ignores them all, and names tracked files quoted, with a `git rm
--cached` command that works and the step to rotate what they held.

`.lpm/.gitignore` lines are read as git reads them, a later negation
needs the rule again, CRLF files keep their line endings, and a
read-only file is left alone. A linked `.lpm` or capture file is
refused, the database is created owner-only before SQLite opens it with
no-follow, and existing capture files are made owner-only. Workflow tests
check the warning appears once and names a tracked capture.

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

A linked sidecar now points at an existing outside file, so following it
would change that file's mode or content. Leftover sidecars are made
owner-only by the preparation step itself, before SQLite opens them. The
corrupt-database workflow test matches the reworded advice.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A repository tracking more capture files than the warning lists gets the
capture pathspec in its `git rm --cached` command instead of every name.

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

Copy link
Copy Markdown
Contributor Author

Review round 1: ledger

Two reviews of f2f0677 and lpm-dev/rust-client-docs#391 at a48dc35: correctness, and security. The security review found that the new git call could run code from a repository. Fixed in c650ea4 to 2edf4d1, and in lpm-dev/rust-client-docs#391 f4edbb6 to 616de8b.

Security review

# Finding Outcome
1 High: git ls-files runs the repository's core.fsmonitor. An archived project's .git/config, or a bare repository a cloned project carries, could run code on lpm tunnel. Fixed: hardened_git_command passes -c core.fsmonitor=false -c safe.bareRepository=explicit, for this check and lpm setup local. Tests: an fsmonitor marker isn't created, and a carried bare repository is refused. The check also runs async, with a 2 s timeout, so a slow repository can't stall the tunnel's start.
2 Medium: trim() treated lines as the rule that git doesn't (leading whitespace, tabs, NBSP, a later !inspector.db). The PR's own test encoded one. Fixed: lines are read as git reads them (a trailing CR and unescaped trailing spaces only), /inspector.db* counts, and a later negation needs the rule again. Tests for each case, the wrong one replaced.
3 Medium: the warning always said .lpm/.gitignore keeps captures out of git. The tracked check missed a linked .lpm and other letter cases. Fixed: git is asked, with ls-files -t --cached --others --exclude-standard and a :(glob,icase) pathspec. The claim prints only when git ignores every capture file. Files git doesn't ignore, or tracks, are named. A linked .lpm is refused.
4 Medium: the git rm --cached .lpm/inspector.db* advice failed while the tunnel ran. Fixed: the warning names the tracked files, quoted for a shell (git rm --cached -- '.lpm/inspector.db'), and says to rotate what they held.
5 Medium: existing sidecars kept 0644, there was a 0644 window at creation, and a linked inspector.db was followed. Fixed: the database is created owner-only before SQLite opens it, and existing capture files are made owner-only. A linked database or sidecar is refused, and SQLite opens with SQLITE_OPEN_NOFOLLOW.
6 Low: writes outside the project through a linked .lpm. Fixed (#3).
7 Low: repository-controlled names unquoted and uncapped; * crossing /. Fixed: names are quoted, at most five are listed, and the pathspec uses glob.
8 Low: no opt-out; a tracked database warns on every run. Kept: the warning stops once the file is untracked.
Docs Overclaimed protection, unquoted command, no rotation step, "0600" overstated, and "Do not commit it" dropped. Fixed in #391.

Correctness review

# Finding Outcome
1 Medium-high: the git rm advice. Fixed (security #4).
2 Medium: the claim always printed. Fixed (security #3).
3 Medium: whitespace matching. Fixed (security #2).
4 Low-medium: a linked .lpm was followed. Fixed (security #3).
5 Low: core.fsmonitor. Fixed (security #1).
6 Low: no test of "prints once" or of the warning end to end. Fixed. Workflow tests require the warning exactly once. They name a tracked capture with its git rm command and no "kept out" claim, and give that claim once git ignores the files.
7 Low (Windows): a read-only .lpm/.gitignore stalled every open for about 6 s. Fixed: it's left alone, and the warning says git doesn't ignore the captures.
Nits CRLF appends; the 64 KiB cap; pub constant; the path printed twice. Fixed: CRLF is kept, the shared 16 MiB config cap applies, the constant is private, and the log line is simpler.
Adjacent install/gitignore.rs ensure_* helpers follow a linked .gitignore. Left for a separate change. Raised with the owner.

Verification

  • CLI gate (2edf4d1): clippy with --all-targets, fmt, build, 7,158 workspace tests, 5,411 CLI unit tests, 119 binary-surface tests, and the 172 run and cache and 103 dev and dev_tunnel workflow tests all pass, with every member rebuilt from the branch.
  • Docs (616de8b): npm run check passes.
  • Mutations: 14 against the new code.
    • Three survived the first run: a linked sidecar followed, leftover sidecars keeping their mode, and CRLF ignored. All three are caught now. The linked-sidecar test points at an existing outside file and checks its mode and content, and a test calls the preparation step directly.
    • One stays equivalent: dropping safe.bareRepository=explicit. ls-files refuses a bare repository anyway ("must be run in a work tree"), so the flag is defense in depth.
  • Also fixed since: the git rm --cached advice listed every tracked name, however many. Past five it now gives the capture pathspec, which untracks every letter case and leaves untracked files alone, as tried in a scratch repository.

Round-2 reviews start now.

This branch has not been deployed

No deployments
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.

1 participant