Add ts dev lint domains and ts dev install-hooks to block non-allowlisted URL hosts - #733
Add ts dev lint domains and ts dev install-hooks to block non-allowlisted URL hosts#733aram356 wants to merge 21 commits into
Conversation
fcff9ed to
30f2cc1
Compare
Port the pure-Rust URL-host linter (`ts dev lint domains`) and the
pre-commit hook installer (`ts dev install-hooks`) onto main's
restructured CLI. Both are cross-host (all git access via gitoxide, no
`git` subprocess) and register as siblings of the macOS-only
`ts dev proxy`.
Adapt to main's layout:
- Move the feature under `src/commands/dev/{lint,install_hooks}` and add
the `Lint` / `InstallHooks` variants to `DevCommand`, replacing the
prior empty-enum handling.
- Reintroduce a small `CliError` (`Io`, `Json`, `EnvironmentError`,
`ViolationsFound`) plus `output::write_{stdout,stderr}_line` /
`write_json`; `output` becomes cross-host while `info` / `warn` stay
macOS-only.
- Map the linter's results to the exit contract (0 clean, 1 violations,
2 environment error) in `commands::dev::run`.
- Scope `error-stack` / `derive_more` cross-host and add `gix` /
`gix-config`; add `assert_cmd` / `predicates` / `temp-env` dev-deps.
The former `serve` subcommand is not restored — main replaced it with
`ts dev proxy` independently of this work.
30f2cc1 to
b0b6bd5
Compare
Pin a fixed `user.name` / `user.email` in the repo-local config of the git fixtures. `create_and_checkout_branch` writes a ref through `repo.reference(...)`, whose reflog needs a committer identity; CI machines have no ambient identity, so both `changed_vs` tests failed with `CreateOrUpdateRefLog(MissingCommitter)`. Developer machines passed only because they inherited a global git identity. Repair the "Resolved by the Phase 2 spike" list in the design spec. Its sub-bullets were glued onto preceding lines and its inline code spans were split across line breaks, so prettier re-indented the block deeper on every run and never converged, failing `format-docs`.
ChristianPavilonis
left a comment
There was a problem hiding this comment.
Automated Review:
Summary
Reviewed the domain-lint and hook-installation changes. I found one confirmed self-exclusion defect, submitted inline.
Findings by priority
- P1: 1 inline finding.
CI
All currently reported PR checks are passing. git diff --check is clean.
Existing Reviews
No existing submitted reviews were returned when checked; this review does not duplicate prior feedback.
prk-Jr
left a comment
There was a problem hiding this comment.
Summary
Adds ts dev lint domains (four scan modes, gitoxide-only) and ts dev install-hooks. The pure-function layer — host extraction, allowlist matching, suppression markers — is well factored and unusually well tested, including userinfo-bypass and rename regressions. Three defects block: the self-exclusion constant points at a path that no longer exists, so the linter flags its own source and this PR fails its own --changed-vs gate; install-hooks silently disables any hooks already in .git/hooks; and both commands error out when run from a subdirectory.
Verified against the PR head in an isolated worktree with a binary built from it (aarch64-apple-darwin): cargo fmt --all -- --check clean, cargo clippy -p trusted-server-cli --all-targets -- -D warnings clean, cargo test -p trusted-server-cli 236 + 28 + 4 + 29 + 1 pass. Evidence for each defect is in the inline comments.
1 of the inline comments below carries a one-click GitHub
suggestion— use Commit suggestion to apply it as a commit on the PR branch. The remaining comments describe the fix in prose because the change spans several files, touchesCargo.lock, or needs a new helper, and can't be auto-applied.
Blocking
🔧 wrench
SELF_PATHpoints at a path that does not exist — self-exclusion is dead — see inline atcrates/trusted-server-cli/src/commands/dev/lint/domains.rs:883install-hookssilently disables every hook already in.git/hooks— see inline atcrates/trusted-server-cli/src/commands/dev/install_hooks.rs:229- Both commands fail from any subdirectory (
gix::opendoes not discover the repo root) — see inline atcrates/trusted-server-cli/src/commands/dev/lint/domains.rs:995
❓ question
- Stage-2 dogfooding policy for the linter's own test file and the two design docs — see the cross-cutting section below.
Non-blocking
♻️ refactor / 🤔 thinking / 🌱 seedling / ⛏ nitpick
- Violation report prints the stale
src/dev/lint/domains.rshint path (suggestion) — see inline atcrates/trusted-server-cli/src/commands/dev/lint/domains.rs:2054 temp-envdev-dependency added but never used — see inline atcrates/trusted-server-cli/Cargo.toml:81- Four new deps bypass
[workspace.dependencies]— see inline atcrates/trusted-server-cli/Cargo.toml:41 - Absolute-URL regex accepts single-label hosts; protocol-relative one does not — see inline at
crates/trusted-server-cli/src/commands/dev/lint/domains.rs:341 test_support.rsandtests/common/mod.rsare byte-identical, kept in sync by comment — see inline atcrates/trusted-server-cli/tests/common/mod.rs:8- Diff modes don't skip binary blobs, full-repo mode does — see inline at
crates/trusted-server-cli/src/commands/dev/lint/domains.rs:950 --stagedwrites a tree object into the object database on every run — see inline atcrates/trusted-server-cli/src/commands/dev/lint/domains.rs:1025
Cross-cutting / body-level findings
-
❓ Stage-2 dogfooding policy. After the
SELF_PATHfix,ts dev lint domains --changed-vs origin/mainon this head still reports 91 violations across 3 of this PR's own new files:crates/trusted-server-cli/tests/lint_domains_cli.rs(30),docs/superpowers/specs/2026-05-18-check-domains-design.md(34),docs/superpowers/plans/2026-05-18-ts-dev-lint-domains.md(27) — all from the deliberatetest.com/evil.com/cdn.example.evilfixtures the spec's own test cases require. What is the intended resolution: extend the exclusion list to the linter's test file anddocs/superpowers/**, sprinkleallow-domain:markers, or accept that the Stage 2 changed-lines gate cannot be turned on until the Stage 1 cleanup covers these? Worth answering in this PR, because it decides whether the exclusion policy needs to grow before the gate exists. -
📌 Plan Phase 8 (documentation) did not ship. The plan's Task 8.1 (
CONTRIBUTING.md— "Pre-commit URL-host linter" install steps) and Task 8.2 (README.mdmention) have no counterpart in the diff, so nothing in the repo tells a contributor thatts dev install-hooksexists or that a pre-commit linter is expected. Either land those two doc edits here or open a follow-up so the install step isn't discoverable only from the design doc. -
📝 PR description's exit-code contract overstates the implementation. The description lists "130 cancelled", but there is no
CliError::Cancelledvariant and no 130 mapping —finish()incommands/dev/mod.rsmapsViolationsFound→ 1,EnvironmentError→ 2, everything else → 1. The spec leaves the 130 model explicitly undecided ("Pick one model"), so the code is self-consistent; only the description is ahead of it. -
👍 Genuinely strong adversarial testing. The userinfo-bypass regressions (
https://github.com@test.com/path→test.com,https://a@b@c.evil→c.evil) close a real allowlist bypass; the pure-rename and rename+edit cases pin a bug that a naive path-map walk would reintroduce; andworks_without_git_on_path(env_clear+ emptyPATH) actually proves the no-subprocess claim rather than asserting it. The suppression-marker bypass tests (allow-domaininside a URL path, host literally namedallow-domain) are the kind of tests reviewers usually have to ask for.
CI Status
- integration tests (Fastly EC lifecycle): PASS
- integration tests: PASS
- browser integration tests: PASS
- CodeQL: PASS
- Analyze (actions): PASS
- Analyze (rust): PASS
- Analyze (javascript-typescript): PASS
- prepare integration artifacts: PASS
- vitest: PASS
- cargo check/build/test (spin native + wasm32-wasip1): PASS
- cargo test: PASS (required)
- cargo test (ts CLI, native): PASS
- cargo test (axum native): PASS
- cargo test (cross-adapter parity): PASS
- cargo check (cloudflare native + wasm32-unknown-unknown): PASS
- format-typescript: PASS (required)
- format-docs: PASS (required)
- cargo fmt: PASS (required)
Blocking fixes from review of #733: - SELF_PATH pointed at the pre-move `src/dev/lint/domains.rs`, so the component-aware `Path::ends_with` never matched and the linter flagged its own fixtures (self-scan 38 violations, `--changed-vs origin/main` 129). Correct the path and sweep every stale spelling across code, tests, and the two design docs. Extend the self-exclusion to the E2E test file and the `docs/superpowers/` design docs, whose deliberate `test.com` / `evil.com` fixtures are the remaining 91 — `--changed-vs origin/main` now reports 0 on this PR's own files. - `install-hooks` set `core.hooksPath` without noticing executable hooks already in `.git/hooks`, silently disabling them. Add a preflight that refuses without `--force` (and prints a note under it), plus tests. - `gix::open` does not discover the repo root, so every mode failed from a subdirectory with a misleading "not a git repository". Switch the three collectors and `install_hooks` to `gix::discover`; add a nested-subdirectory regression test. Non-blocking cleanups: - Skip non-UTF-8 blobs in diff modes, matching full-repo's binary skip. - Drop the unused `temp-env` dev-dependency. - Move the `gix` / `gix-config` / `assert_cmd` / `predicates` version pins into `[workspace.dependencies]` so the pin is enforceable. - De-duplicate the fixture helpers: `tests/common/mod.rs` now includes the single `test_support.rs` via `#[path]` instead of a byte-identical copy kept in sync by comment. - Document the `--staged` tree-object write and the intentional single-label-host asymmetry in the spec. - Land the Phase 8 docs: `ts dev install-hooks` steps in CONTRIBUTING.md and a mention in README.md.
Blocking fixes from review of #733: - SELF_PATH pointed at the pre-move `src/dev/lint/domains.rs`, so the component-aware `Path::ends_with` never matched and the linter flagged its own fixtures (self-scan 38 violations, `--changed-vs origin/main` 129). Correct the path and sweep every stale spelling across code, tests, and the two design docs. Extend the self-exclusion to the E2E test file and the `docs/superpowers/` design docs, whose deliberate `test.com` / `evil.com` fixtures are the remaining 91 — `--changed-vs origin/main` now reports 0 on this PR's own files. - `install-hooks` set `core.hooksPath` without noticing executable hooks already in `.git/hooks`, silently disabling them. Add a preflight that refuses without `--force` (and prints a note under it), plus tests. - `gix::open` does not discover the repo root, so every mode failed from a subdirectory with a misleading "not a git repository". Switch the three collectors and `install_hooks` to `gix::discover`; add a nested-subdirectory regression test. Non-blocking cleanups: - Skip non-UTF-8 blobs in diff modes, matching full-repo's binary skip. - Drop the unused `temp-env` dev-dependency. - Move the `gix` / `gix-config` / `assert_cmd` / `predicates` version pins into `[workspace.dependencies]` so the pin is enforceable. - De-duplicate the fixture helpers: `tests/common/mod.rs` now includes the single `test_support.rs` via `#[path]` instead of a byte-identical copy kept in sync by comment. - Document the `--staged` tree-object write and the intentional single-label-host asymmetry in the spec. - Land the Phase 8 docs: `ts dev install-hooks` steps in CONTRIBUTING.md and a mention in README.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review addressed — a05cf2aThanks for the thorough pass. All three blocking findings and the non-blocking cleanups are resolved; each inline thread has a reply. Summary of the cross-cutting / body-level items: Stage-2 dogfooding policy (❓). Resolved by extending the self-exclusion rather than sprinkling markers. After the Plan Phase 8 docs (📌). Landed here: CONTRIBUTING.md gains a "Pre-commit URL-host linter" section with the PR description exit-code (📝). Corrected — the description no longer claims "130 cancelled"; it now matches Verified on |
Resolve the Cargo.lock conflict by re-resolving against the merged manifests. Pin kstring to 2.0.2: 2.0.4 raises its MSRV to rustc 1.96, which the workspace's pinned 1.95.0 toolchain cannot build. kstring arrives transitively via gix-config.
The plan linked the spec as a sibling, but it lives in `specs/`, not `plans/`. VitePress builds with the dead-link gate main added in #1066, so this failed the `format-docs` job. Use the `../specs/` form the rest of `docs/superpowers/` already uses.
7c0cc50 to
1c2914a
Compare
ChristianPavilonis
left a comment
There was a problem hiding this comment.
Summary
Reviewed head 026fafcbc8fc6a627e38d309e400d2b47ff34a94 against 125ed242ec6925f2b41f9c84addf75e61b08ae1c. Requesting changes because runtime probes found a branch-controlled hook execution path, URL and rename false negatives in the linter, and installer safety failures not covered by the green CI suite. No repository files were modified during review.
prk-Jr
left a comment
There was a problem hiding this comment.
Summary
Reviewed head 026fafcb against merge-base 125ed242 in an isolated worktree, with a binary built from this head used for runtime probing. The linter and installer are carefully built — the userinfo-bypass handling, the unused-suppression warnings, and the honest in-code note about --staged writing a real tree object all show real care, and 117 tests is serious coverage.
This review covers only findings not already raised in the open review on this head; I am not restating those threads. Requesting changes on two of my own: the installer leaves an untracked, un-gitignored artifact carrying a machine-specific absolute path in every contributor's checkout, and the CONTRIBUTING prose this PR adds overstates a safety guarantee that a runtime probe contradicts.
2 of the inline comments below carry a one-click
suggestion. Both were applied to a scratch worktree in isolation and clearedcargo fmt --all -- --check,cargo clippy -p trusted-server-cli --target aarch64-apple-darwin --all-targets -- -D warnings, and the fullcargo test -p trusted-server-clisuite (262 lib + 5 + 28 + 29 + 1, zero failures), with a byte-exact pre/post-verification patch check showing no drift. The remaining comments describe the fix in prose because it spans multiple files or depends on how another open thread resolves.
No repository files were modified by this review.
Blocking
🔧 wrench
.githooks/is untracked and un-gitignored — see inline atcrates/trusted-server-cli/src/commands/dev/install_hooks.rs:228- CONTRIBUTING overstates the installer's refusal guarantee — see inline at
CONTRIBUTING.md:132
Non-blocking
♻️ refactor
resolve_base_refrejects object ids and revision expressions (suggestion) — see inline atcrates/trusted-server-cli/src/commands/dev/lint/domains.rs:1191set_local_config_valuewould overwrite an unparsable.git/config(suggestion) — see inline atcrates/trusted-server-cli/src/commands/dev/install_hooks.rs:170- Spec and code disagree on the exclusion list — see inline at
docs/superpowers/specs/2026-05-18-check-domains-design.md:527
🤔 thinking / 📝 note / ⛏ nitpick
- Underscore is a further authority bypass — see inline at
crates/trusted-server-cli/src/commands/dev/lint/domains.rs:341 --format jsonechoes the whole source line, and.env*is in scope by design — see inline atcrates/trusted-server-cli/src/commands/dev/lint/domains.rs:1993- Extension matching is case-sensitive — see inline at
crates/trusted-server-cli/src/commands/dev/lint/domains.rs:946 --stagedtree synthesis is not mode-faithful — see inline atcrates/trusted-server-cli/src/commands/dev/lint/domains.rs:1050FileViolation's serde renames invert intuition — see inline atcrates/trusted-server-cli/src/commands/dev/lint/domains.rs:1987write_atomicleaks its temp file on rename failure and never fsyncs — see inline atcrates/trusted-server-cli/src/commands/dev/install_hooks.rs:124
Cross-cutting / body-level findings
-
🤔 85 new crates, and git network transport ships inside
ts.cargo tree -p trusted-server-cli --target aarch64-apple-darwin -e normal -i gix-transportresolves throughgix -> gix-protocol -> gix-transport, so the git wire protocol and transport layer are compiled into the shippedtsbinary for a tool that never opens a socket. Credit where due:default-features = falsedid real work —gix-archive,gix-blame,gix-statusandgix-mergeare lockfile-only and never built, there are no duplicate versions (the spec'scargo treeclaim holds), andzlib-rsis pure Rust with no C or OpenSSL pulled in. Depending on the granular subcrates actually used (gix-discover,gix-index,gix-diff,gix-object,gix-config) instead of the umbrellagixcrate would drop the transport surface. CLAUDE.md asks for justification on new dependencies; 85 crates for a dev-workflow linter is worth an explicit line in the spec either way. -
🤔 Shell scripts are out of scope, and that is where network calls actually live.
.sh,Makefile,.jsx,.pyand.scssall fall throughpath_is_scanned. This repo has 10 tracked.shfiles, 8 of which containhttp(s)://URLs. The spec documents the scanned-extension list explicitly, so this is a deliberate design boundary rather than a bug — but a linter whose stated purpose is catching non-allowlisted egress hosts skippingscripts/*.shis worth revisiting before Stage 2, or at least recording as a known gap in the spec's threat model. -
🌱 No
ts dev install-hooks --uninstall. Installation is a documented one-liner in both README and CONTRIBUTING; removal requires knowing to unsetcore.hooksPathand delete.githooks/by hand. The restore hint only prints when a previous value was displaced, so a contributor installing into a clean checkout is told nothing about how to back out. -
📝 A concern I raised and then withdrew, recorded so it does not resurface.
added_linesrunsDiff::compute(Algorithm::Myers, ...)with no size cap, which looked like a pre-commit latency risk on large staged files. Measured on a 9.1 MB / 120,000-line staged.tsfile: 2.9 s in an unoptimised debug build. Not a problem, and not a finding. -
👍 Praise. The userinfo-bypass handling in both regexes, with named regression tests covering
user:password@, multiple@, and the protocol-relative form, is the kind of thing that usually gets missed. So is the unused-suppression-marker warning, which stopsallow-domain:from silently rotting. The in-code note thatwrite_index_to_treepersists a real tree object into.git/objectsdocuments a real cost honestly instead of hiding it. And resolving the earlier duplicated-fixture thread with#[path]rather than a sync-by-comment copy is the right call.
CI Status
All 19 reported checks PASS at this head. Required checks are marked.
- Analyze (actions): PASS
- Analyze (javascript-typescript): PASS
- Analyze (rust): PASS
- CodeQL: PASS
- browser integration tests: PASS
- cargo check (cloudflare native + wasm32-unknown-unknown): PASS
- cargo check/build/test (spin native + wasm32-wasip1): PASS
- cargo fmt: PASS (required)
- cargo test: PASS (required)
- cargo test (axum native): PASS
- cargo test (cross-adapter parity): PASS
- cargo test (ts CLI, native): PASS
- format-docs: PASS (required)
- format-typescript: PASS (required)
- integration tests: PASS
- integration tests (Fastly EC lifecycle): PASS
- prepare integration artifacts: PASS
- vitest: PASS
…e.hooksPath The installer used to write `.githooks/pre-commit` into the working tree and set `core.hooksPath = .githooks` in `.git/config`. Review found that this made git execute whatever hooks a checked-out branch carried under `.githooks/`, left an un-ignored file with a machine-specific path in every checkout, and depended on a config write whose linked-worktree, include-scope, and file-permission semantics were wrong. Write the hook into `<common git dir>/hooks/pre-commit` instead and never touch git configuration. Preflight against the effective `core.hooksPath` from every config scope and refuse when it is set, regardless of --force. Reject a symlinked hooks directory, detect an existing hook with symlink_metadata so dangling symlinks count, and under --force back it up without parsing it. Write the hook atomically with mode 0755, fsync, and temp-file cleanup on failure. Drop the now-unused gix-config dependency, update CONTRIBUTING and the design spec, and add real-git end-to-end tests proving a branch-planted post-checkout hook stays inert, a linked worktree shares the installed hook, and a global core.hooksPath is refused.
The host regexes stopped at the first character outside an ASCII letter/digit/dot/hyphen class, so `https://github.com%2eevil.com`, `https://github.com。evil.com` and `https://github.com_evil.com` each handed the allowlist the prefix `github.com` and passed. JSON's escaped solidus (`https:\/\/partner.com`) was not recognised as a URL at all. Capture the complete authority with a class that admits everything WHATWG host parsing turns into a host (percent escapes, underscores, non-ASCII letters and marks, ideographic full stops, a port), unescape `\/` before matching, and parse the authority with the `url` crate so the allowlist sees the canonical hostname: percent-decoded, IDNA-mapped, lowercased, port and trailing dot dropped. An authority the parser rejects is reported as written rather than trimmed to an allowlisted prefix. The full-repo audit reports the same 437 pre-existing violations as before and lines added versus main stay clean, so the wider class does not introduce false positives on this codebase.
A pure rename diffs the old blob against the new one, so moving an unchanged `legacy.txt` to `src/endpoint.ts` produced no added lines and both `--staged` and `--changed-vs` passed content that had never been scanned. Use the rewrite's source location: when the source path was outside the scanned set and the destination is inside it, diff against an empty source so every newly in-scope line is checked. Cover the boundary in both modes.
…ion case The whole `docs/superpowers/` tree (over a hundred files) was exempt from the Markdown policy although only this linter's spec and plan needed it. List those two documents in SELF_EXCLUDED_PATHS next to the source and E2E test file, drop the directory match, and bring the spec's "Always excluded (paths)" list in line with the code, since the code cites it as its authority. Compare extensions and lockfile basenames ASCII case-insensitively so a `README.MD` or `config.JSON` committed from a case-insensitive checkout stays in scope.
`gix::init` names the initial branch after `init.defaultBranch`, so with a global `master` setting four `--changed-vs main` tests failed. Write `HEAD` explicitly after init and expose `init_repo_with` so a test can inject `init.defaultBranch=master` and prove the fixture ignores it.
- `--changed-vs` falls back to a full revspec after the named-ref ladder, so raw object ids and expressions like `HEAD~1` resolve while a bare branch name keeps its remote-tracking priority. - The JSON report redacts any URL `userinfo@` span from the line excerpt; `.env*` files are scanned by design and the report is archived by CI. - `FileViolation` field names now match the JSON keys, removing the inverted serde renames without changing the output shape. - The synthesized index tree keeps the executable bit and skips conflict stages, so `--staged` mid-merge scans resolved entries only instead of an arbitrary side.
Review addressed at 7077b7aBoth rounds are covered; every inline thread has a reply naming its commit. Seven of the installer findings traced to one design choice, so that is the first commit:
Body-level items: the 85-crate footprint is now justified in the spec's dependency notes ( Full-repo audit is diagnostic and now reports 485 pre-existing hits on the merged tree (the |
prk-Jr
left a comment
There was a problem hiding this comment.
Summary
The installer redesign addresses most prior findings, but real Git/CLI reproductions show four remaining problems in commit-index selection, complete-host extraction, credential redaction, and hooksPath handling.
Blocking
- 🔧 P1 — Read the index Git supplies to the pre-commit hook — see inline at
crates/trusted-server-cli/src/commands/dev/lint/domains.rs:1195. - 🔧 P1 — Finish extracting the authority before canonicalizing it — see inline at
crates/trusted-server-cli/src/commands/dev/lint/domains.rs:558. - 🔧 P2 — Redact userinfo in the escaped URLs the scanner now recognizes — see inline at
crates/trusted-server-cli/src/commands/dev/lint/domains.rs:2322. - 🔧 P2 — Preserve an explicitly empty hooksPath during preflight — see inline at
crates/trusted-server-cli/src/commands/dev/install_hooks.rs:174.
Validation and scope
The original-head CLI build, formatting, 195 focused unit tests, and 32 CLI E2E tests passed. Fresh fixture repositories reproduced all four findings. The illustrative index, redaction, and hooksPath fixes were tested individually, but the full cross-target release matrix was not rerun; they remain manual proposals, not one-click suggestions. Linux-only and Windows behavior were not tested locally.
CI Status
- integration tests (Fastly EC lifecycle): PASS
- browser integration tests: PASS
- integration tests: PASS
- CodeQL: PASS
- cargo test (ts CLI, native): PASS
- Analyze (actions): PASS
- Analyze (javascript-typescript): PASS
- format-docs: PASS (required)
- format-typescript: PASS (required)
- cargo test (axum native): PASS
- Analyze (rust): PASS
- cargo check (cloudflare native + wasm32-unknown-unknown): PASS
- cargo test: PASS (required)
- cargo check/build/test (spin native + wasm32-wasip1): PASS
- cargo test (cross-adapter parity): PASS
- cargo fmt: PASS (required)
- vitest: PASS
- prepare integration artifacts: PASS
- Analyze (javascript-typescript): PASS
Read the index git supplies to the hook. Git points GIT_INDEX_FILE at a
temporary index for `git commit -a` and `git commit -- <path>`, so
reading <git dir>/index inspected the pre-commit state and let both
forms commit violations that `git add` + `git commit` rejected.
Extract the whole authority before canonicalizing it. The authority is
now taken up to a real URL/source delimiter instead of an allow-list of
host-ish characters, and the protocol-relative pattern no longer ends in
a literal .[A-Za-z]{2,} that backtracks into an allowlisted prefix when
the real suffix is percent-encoded. The dotted-suffix rule that filtered
comment dividers moves to the canonical host, where %2e is already a
dot. This catches //github.com%2eevil%2ecom, https://github.com<emoji>.com
and https://%65vil.com, which all reported no violation.
Redact userinfo behind escaped soliduses. The extraction paths unescape
\/ before matching a host, but the redactor only recognized literal //,
so the JSON report echoed credentials from escaped JSON URLs.
Treat an empty core.hooksPath as the override it is. Git stops reading
.git/hooks for an empty value, so filtering it out let the installer
report success while installing nothing.
A full-repo audit reports exactly the same 484 violations as before,
so the wider authority capture adds no false positives.
Closes #1160
Summary
ts dev lint domains— a pure-Rust source/config/docs linter that flags non-allowlisted URL hosts in four modes (--staged,--changed-vs <ref>, full-repo, explicit paths). All git operations go through gitoxide (no shelling out togit). Captured URL authorities are canonicalised through theurlcrate (percent-decoding, IDNA, case, port) before the allowlist check, so lookalike authorities cannot pass on a prefix match.ts dev install-hooks— writes a managedpre-commithook that runsts dev lint domains --stagedinto git's own.git/hooksdirectory (the main repository's from a linked worktree). It never edits git configuration or the working tree; it refuses whencore.hooksPathis set in any scope, refuses to clobber an unmanaged hook, and with--forcebacks the old hook up untouched.ts devleaf into a subcommand group;lintandinstall-hookssit next toproxy.Design
Allowlists:
EXACT_HOSTS,SUBDOMAIN_HOSTS,REFERENCE_HOSTS, plus RFC 2606 reserved TLDs. Suppression marker:// allow-domain: host(and#,<!--,*comment forms). Scanned extensions cover Rust, TS/JS, configs, Markdown, CSS, HTML, env files, and Dockerfiles, matched case-insensitively. Only the linter's own source, E2E suite, spec and plan are exempt. Exit codes: 0 clean, 1 violations, 2 environment error.Evasion and bypass hardening
Review found four ways the tool could be defeated while reporting success. All are fixed, each with a regression test:
<git dir>/index, but git supplies a temporary index viaGIT_INDEX_FILEforgit commit -aandgit commit -- <path>, so both forms committed violations thatgit add+git commitrejectedGIT_INDEX_FILE, falling back torepo.index_path()//github.com%2eevil%2ecom,https://github.com.com(an invisible soft hyphen) andhttps://%65vil.com— the first two reported an allowlisted prefix, the third nothing at allhttps:\/\/user:pw@host)core.hooksPathwas treated as unset, so installation reported success while git never read.git/hooksThe wider authority capture is the change most likely to cause false positives, so it was checked against the whole repository: a full-repo audit reports exactly the same 484 violations as the pre-change build, byte-identical — no false positives, no lost detections.
Test plan
cargo fmt --all -- --checkcargo clippy --package trusted-server-cli --target <host> --all-targets --all-features -- -D warningscargo clippy-fastly,cargo clippy-axumcargo test --package trusted-server-cli --target <host>— 351 tests across 8 suites, including real-gitend-to-end tests for the installer (branch-planted.githooks/post-checkoutstays inert, linked worktree shares the hook, global and emptycore.hooksPathrefused, temporary-index commit forms blocked)ts dev lint domains --changed-vs origin/mainon this branch: cleants dev lint domainsfull audit: diagnostic only, reports the repo's pre-existing violations (no CI gate in v1)cd docs && npm run format