Skip to content

fix(sandbox): classify git push as network access - #726

Open
PierrunoYT wants to merge 13 commits into
Gitlawb:mainfrom
PierrunoYT:fix/issue-703-git-push-network
Open

fix(sandbox): classify git push as network access#726
PierrunoYT wants to merge 13 commits into
Gitlawb:mainfrom
PierrunoYT:fix/issue-703-git-push-network

Conversation

@PierrunoYT

@PierrunoYT PierrunoYT commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

Summary

  • classify git push as network-sensitive before sandbox execution
  • ensure approving the permission prompt enables the temporary network profile immediately
  • cover custom transports such as gitlawb:// in analyzer and risk-classifier tests

Fixes #703.

Validation

  • go test ./...
  • go vet ./...
  • GOTOOLCHAIN=go1.26.5 go run golang.org/x/vuln/cmd/govulncheck@v1.3.0 ./...
  • diff-scoped golangci-lint: 0 issues

The repository-wide lint command still reports 35 pre-existing findings unrelated to this change.

Summary by CodeRabbit

  • Bug Fixes
    • Improved Git “network” verb detection when Git is invoked with value-taking global options (for example -C, --git-dir), including git.exe/git.cmd.
    • Correctly classifies git archive as network only when a --remote is targeted, while keeping local archive non-network.
    • Strengthened fallback handling for obfuscated/unparseable shell commands (including sh -c payload parsing) and aligned Windows command token normalization for executable suffixes.
  • Tests
    • Expanded risk-hardening and regression coverage for Git push/network blocking and wrapper-obfuscated scenarios, plus additional inline-option and parsing edge cases.

@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: fb4afc51-ffee-459a-b04e-1f84cd6e23e2

📥 Commits

Reviewing files that changed from the base of the PR and between 6225826 and 0b56cd6.

📒 Files selected for processing (4)
  • internal/sandbox/analyzer.go
  • internal/sandbox/analyzer_test.go
  • internal/sandbox/risk.go
  • internal/sandbox/risk_hardening_test.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • internal/sandbox/analyzer.go
  • internal/sandbox/risk.go
  • internal/sandbox/risk_hardening_test.go

Walkthrough

Git network detection now handles value-taking global options, remote archives, wrappers, and platform-specific Git executables. Parsed and unparseable network commands receive network classification, with regression coverage for network grants and approved git push execution.

Changes

Sandbox network classification

Layer / File(s) Summary
Git network detection and analyzer coverage
internal/agent/command_prefix.go, internal/agent/command_prefix_test.go, internal/sandbox/analyzer.go, internal/sandbox/analyzer_test.go, internal/sandbox/safe_command.go
Git option parsing, subcommand detection, remote archive handling, and executable normalization cover separate and inline values, local commands, and Windows executable variants.
Risk classification for parsed and unparseable Git commands
internal/sandbox/risk.go, internal/sandbox/risk_hardening_test.go, internal/sandbox/engine_test.go
Fallback tokenization and Git-specific matching classify network activity through wrappers and shell payloads while preserving non-network results for local commands and option-like tokens.
Approved git push execution coverage
internal/agent/loop_test.go
The regression test verifies that an approved git push receives the network grant and executes with the expected fake Git output.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Agent
  participant SandboxClassifier
  participant GitAnalyzer
  participant GitProcess
  Agent->>SandboxClassifier: request git push
  SandboxClassifier->>GitAnalyzer: classify Git subcommand
  GitAnalyzer-->>SandboxClassifier: critical network risk
  SandboxClassifier-->>Agent: request network approval
  Agent->>GitProcess: execute approved git push
  GitProcess-->>Agent: return command output
Loading

Suggested reviewers: gnanam1990, vasanthdev2004, anandh8x

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.61% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Concise and specific; it accurately describes the main change to treat git push as network access.
Linked Issues check ✅ Passed The changes address #703 by classifying git push/Git network commands before sandbox execution so the network grant is applied.
Out of Scope Changes check ✅ Passed The changes stay focused on Git/network sandbox classification and related regressions; no clearly unrelated code was introduced.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 18, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the sandbox command analyzer/risk classifier to treat git push as network-sensitive, with added tests to ensure the AST-based analyzer flags it even when the command doesn’t contain an obvious URL.

Changes:

  • Extend commandUsesNetwork to classify git push as network access.
  • Add analyzer coverage for git push (and a non-network git commit) in AnalyzeCommand tests.
  • Add a risk-classifier hardening test asserting git push is flagged as critical+network when regex-based detection would miss it.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
internal/sandbox/analyzer.go Expands git subcommand network detection to include push.
internal/sandbox/analyzer_test.go Adds AnalyzeCommand test cases for git push and a local-only git commit.
internal/sandbox/risk_hardening_test.go Adds a hardening test to ensure AST-based classification flags git push as network.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/sandbox/analyzer_test.go Outdated
Comment thread internal/sandbox/risk_hardening_test.go Outdated
Comment thread internal/sandbox/analyzer.go Outdated
Comment thread internal/sandbox/analyzer.go Outdated
Comment thread internal/sandbox/analyzer.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
internal/sandbox/risk_hardening_test.go (1)

297-309: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer t.Errorf over t.Fatalf in loops.

Using t.Fatalf inside a loop will immediately abort the test on the first failure, which prevents the remaining test cases from executing. Replacing it with t.Errorf allows all cases to be evaluated even if one fails.

♻️ Proposed refactor
 	for _, command := range []string{
 		`curl https://example.com && "unterminated`,
 		`git fetch origin && "unterminated`,
 		`git pull origin main && "unterminated`,
 		`git push gitlawb://example.com/repo.git main && "unterminated`,
 	} {
 		risk := classifyCommand(command)
 		if !HasRiskCategory(risk, "unparseable_command") {
-			t.Fatalf("Classify(%q) = categories %v; want unparseable_command", command, risk.Categories)
+			t.Errorf("Classify(%q) = categories %v; want unparseable_command", command, risk.Categories)
 		}
 		if risk.Level != RiskCritical || !HasRiskCategory(risk, "network") {
-			t.Fatalf("Classify(%q) = level %s, categories %v; want critical network", command, risk.Level, risk.Categories)
+			t.Errorf("Classify(%q) = level %s, categories %v; want critical network", command, risk.Level, risk.Categories)
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/sandbox/risk_hardening_test.go` around lines 297 - 309, In the
table-driven loop testing classifyCommand, replace both t.Fatalf calls with
t.Errorf so each command case is evaluated even when an earlier assertion fails.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@internal/sandbox/risk_hardening_test.go`:
- Around line 297-309: In the table-driven loop testing classifyCommand, replace
both t.Fatalf calls with t.Errorf so each command case is evaluated even when an
earlier assertion fails.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 45f38035-3958-4d77-b84f-f163855e8c49

📥 Commits

Reviewing files that changed from the base of the PR and between 7a22945 and d72a192.

📒 Files selected for processing (5)
  • internal/agent/loop_test.go
  • internal/sandbox/analyzer.go
  • internal/sandbox/analyzer_test.go
  • internal/sandbox/risk.go
  • internal/sandbox/risk_hardening_test.go

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 18, 2026

@Vasanthdev2004 Vasanthdev2004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this, and the direction is right (git push should be network-gated). But the AST classifier still misses the most common git form, so I would like a fix before it lands.

The git branch of commandUsesNetwork calls firstSubcommand, which skips only dash-prefixed and numeric tokens. git's value-consuming global options put their value in the NEXT token, so firstSubcommand returns that value as the "subcommand." I ran the classifier against the current head:

git push origin main              Network=true   Risk=critical  network   <- correct
git -C repo push origin main      Network=false  Risk=high      (none)    <- missed
git -c http.sslVerify=false push  Network=false  Risk=high      (none)    <- missed
git --git-dir /x/.git push        Network=false  Risk=high      (none)    <- missed
git.exe push origin main          Network=false  Risk=high      (none)    <- missed

These all parse cleanly, so TooComplex stays false and the unparseable-pattern fallback never runs. So git -C <dir> push (the canonical form for operating on a repo without cd) classifies as plain shell, not network, and its risk drops from Critical to High.

To be fair on severity: this is not an always-open egress hole. When the sandbox backend is provisioned, the runtime deny-by-default still blocks the socket and raises the network prompt via ReasonNetworkBlocked, so the classifier is defense-in-depth there. But it becomes a real unprompted-egress path when the backend is unavailable or degraded, and the Critical-to-High mis-level can flip auto-allow in the more permissive autonomy modes regardless. Since the whole point of the PR is to classify these, I would rather close the gap than ship a gate that misses the most common invocation.

The fix looks small:

  • In the git case, skip the values of git's space-separated value-consuming globals (-C, -c, --git-dir, --work-tree, --namespace, --exec-path, --super-prefix) before taking the subcommand. The joined --git-dir=/x form is already fine since it is one dash-prefixed token.
  • Normalize a .exe program token so git.exe is treated as git.
  • Add a PARSEABLE regression test: classifyCommand("git -C repo push origin main") should be RiskCritical with the network category. Right now the only -C test is the one with the trailing && "unterminated, which forces the unparseable path and masks this AST gap.

Otherwise the wiring is fine, and build/vet/gofmt are clean locally. Happy to re-review quickly once the AST path handles the option forms.

gnanam1990
gnanam1990 previously approved these changes Jul 21, 2026

@gnanam1990 gnanam1990 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APPROVE — the core fix is correct and well-covered: git clone|fetch|pull|push now classify as network access on the primary AST path, verified end-to-end (git push origin mainNetwork=true, git commit -m x stays Network=false), and the hardened regex fallback fails closed on unparseable variants. The one remaining gap is a minor consistency issue, not merge-blocking.

Nice work: the AST change (analyzer.go:153) and the fallback hardening (risk.go:36, now git(?:\s+[^\s;&|]+){0,8}\s+(clone|fetch|pull|push)) are both landed, and the new tests are real — TestAnalyzeCommand (git fetch/pull/push-custom-transport → network), TestClassifyASTCatchesNetworkProgramsRegexMisses, and TestClassifyUnparseableNetworkCommandFailsClosed including the git -C repo push … && "unterminated fail-closed case. All PR-relevant classification tests pass locally.


[Minor] AST path doesn't skip git global options, so git -C <dir> push isn't classified as network — inconsistent with the fallback you just hardened

internal/sandbox/analyzer.go:153 (root cause: firstSubcommand, analyzer.go:243) — pre-existing blind spot, PR-introduced inconsistency

The three reported findings all collapse to this single root cause. firstSubcommand skips dash-prefixed tokens but treats the next bare token as the subcommand, so for git -C /repo push origin main (words [-C, /repo, push, …]) it returns /repo, not push. The git case then returns Network=false. Runtime probe on the PR HEAD:

git push origin main                      Network=true  TooComplex=false
git -C /repo push origin main             Network=false TooComplex=false   <- gap
git -c http.proxy=x push origin main      Network=false TooComplex=false   <- gap
git -C /repo fetch origin                 Network=false TooComplex=false   <- gap
git -C /repo pull origin main             Network=false TooComplex=false   <- gap
git --git-dir=/repo/.git push origin main Network=true  TooComplex=false   (caught: --foo starts with '-')

The gap is specifically the space-separated value-taking global options (-C <path>, -c <name=value>, --git-dir <path>, --work-tree <path>, --namespace <ns>, --exec-path <path>). Because these commands parse cleanly (TooComplex=false), the hardened unparseableNetworkPattern at risk.go:36 is never consulted — that branch is gated on analysis.TooComplex at risk.go:132. So the fallback now tolerates git -C repo push but the primary AST path does not: the two paths disagree, and the network category the PR exists to add is silently omitted for the very common git -C <dir> push/fetch/pull form.

Impact is bounded — this is not a network-exfiltration bypass. Network enforcement mode is derived from policy.Network via NormalizeNetworkMode at profile.go:109, decoupled from the analyzer, and the auto-allow branch at engine.go:410 is gated on shellSandboxActive → NativeIsolation. So a misclassified git -C . push still runs wrapped by the platform sandbox with NetworkDeny enforced at the syscall level (the connect() is blocked and the agent reactively prompts), and where no native sandbox is active it prompts anyway via the general path rather than auto-allowing. The only real-world effect is degrading a proactive ReasonNetworkBlocked prompt (engine.go:355/357) into a reactive/generic one, plus the AST↔regex inconsistency.

Provenance: the underlying firstSubcommand blind spot is pre-existing — base analyzer.go:153 was firstSubcommand(words, nil) == "clone" and had the same hole for git -C <dir> clone. What this PR introduces is the inconsistency: it extended classification to push/fetch/pull and explicitly closed the -C gap in the regex fallback (and tests it), but left the primary AST path unfixed.

Suggested fix: give the git case a dedicated subcommand resolver that consumes git's global value-taking options before reading the subcommand (-C <path>, -c <name=value>, --git-dir, --work-tree, --namespace, --exec-path in their separate-token form), mirroring the tolerance already in unparseableNetworkPattern, then test the resolved token against {clone,fetch,pull,push}. Add git -C repo push origin main (plus -c / fetch / pull variants) to TestClassifyASTCatchesNetworkProgramsRegexMisses and TestAnalyzeCommand — those assertions fail today and would pin the fix.


Tests: go build ./..., go vet on the touched packages, and gofmt -l are clean; all PR-relevant classification/agent tests pass. The failing tests in internal/sandbox and internal/agent (path/symlink/out-of-workspace under /private/tmp) are pre-existing environmental failures that reproduce identically on base — not PR-attributable.

Merge is kevin's call per the program gate.

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found issues that need to be addressed before this is ready.

Findings

  • Resolve the merge conflict with main
    GitHub currently reports this PR as CONFLICTING / DIRTY, so it cannot be merged or tested in its target-branch composition. Rebase or merge the current target branch and resolve the conflict before requesting another review.

  • [P2] Classify Git commands after consuming global-option values
    internal/sandbox/analyzer.go:153
    The generic firstSubcommand skips -C/-c/--git-dir themselves but treats their following values as the subcommand. Consequently ordinary, parseable commands such as git -C repo push origin main, git -c http.proxy=x fetch origin, and git --git-dir /repo/.git pull produce no network risk. Because they parse successfully, the new regex fallback is not consulted, and the engine skips its required proactive network-approval path. Use a Git-aware resolver that consumes value-taking global options (as internal/agent/command_prefix.go already does) and add parseable regression coverage.

  • [P2] Recognize the Windows git.exe command spelling
    internal/sandbox/analyzer.go:152
    effectiveProgram normalizes git.exe to git.exe, not git, so git.exe push origin main never reaches this new Git network classifier. It is parseable, so the fallback cannot repair the miss and the command does not receive the intended proactive network prompt. Normalize executable suffixes (or explicitly handle git.exe) and cover that spelling in the analyzer and risk tests.

…ush-network

main's #7xx already classifies git clone/fetch/pull/push/ls-remote/archive as
network in gitUsesNetwork, so the conflict resolves to main's version and this
branch keeps only what is still missing:

- gitSubcommand resolves the subcommand past git's value-taking global options
  (-C, -c, --config-env, --exec-path, --git-dir, --namespace, --super-prefix,
  --work-tree). firstSubcommand treated their VALUE as the subcommand, so
  `git -C repo push origin main` — the canonical form for operating on a repo
  without cd — classified as plain shell with no network category. Those
  commands parse cleanly, so the unparseable-command regex fallback never ran.
- effectiveProgram trims a trailing .exe, so `git.exe push` (and curl.exe,
  wget.exe, ...) reach the same classification as the bare name. Only .exe is
  trimmed; a .bat/.cmd of the same stem is a different script.
- Parseable regression coverage for both, in the analyzer table and a new
  risk test that asserts TooComplex is false so the fallback cannot mask a
  future AST gap, plus the local-work cases that must stay off the network path.

The hardened fallback regex, the t.Errorf-in-subtests conversion, and the
end-to-end approved-git-push network-grant test come from this branch unchanged.
@PierrunoYT

Copy link
Copy Markdown
Contributor Author

Merged current main (595c292) — the PR is MERGEABLE again — and closed the AST gap everyone converged on.

Resolve the merge conflict with main — done. main meanwhile grew gitUsesNetwork, already covering clone|fetch|pull|push|ls-remote|archive, so the conflict resolves to main's version and this branch now carries only what is still missing on top of it.

[P2] Classify Git commands after consuming global-option values (@jatmn, @gnanam1990, @Vasanthdev2004 — all three findings share this root cause) — fixed with a git-aware gitSubcommand that skips the values of git's space-separated globals (-C, -c, --config-env, --exec-path, --git-dir, --namespace, --super-prefix, --work-tree), mirroring gitOptionConsumesValue in internal/agent/command_prefix.go as suggested. Joined forms (--git-dir=/x, -C/x) were already fine as single dash-prefixed tokens. Verified against the exact matrix from the reviews:

command before after
git -C repo push origin main Network=false, High Network=true, Critical + network
git -c http.sslVerify=false push Network=false, High Network=true, Critical + network
git --git-dir /repo/.git fetch origin Network=false, High Network=true, Critical + network
git --work-tree /repo pull origin main Network=false, High Network=true, Critical + network
git -C repo commit -m x no network no network (unchanged)

[P2] Recognize the Windows git.exe spellingeffectiveProgram now trims a trailing .exe after normalizeProgramToken, so git.exe push classifies like git push; the same fix covers curl.exe, wget.exe, and the other program tables. Deliberately .exe only — a .bat/.cmd of the same stem is a separate script, not the program it is named after, so treating it as that program would be a guess. The command-prefix tables that list powershell.exe explicitly are a different code path and are untouched.

Parseable regression coverage — the key point from @Vasanthdev2004's review was that the only -C test forced the unparseable path and masked the AST gap. New TestClassifyParseableGitNetworkCommandsUseASTPath asserts AnalyzeCommand(...).TooComplex == false for each case before checking Critical + network, so the hardened fallback regex cannot mask a future AST regression, and it also pins the local-work cases (git -C repo commit, git -C repo status, git.exe commit) as non-network. The analyzer table gained the same forms.

@copilot: "the approved-prompt behavior may need an execution-path change, not just classification" — checked, and no execution change is needed. The turn network grant already applies on approval; the missing piece was purely classification, so the command never reached that path. TestRunApprovedGitPushPromptAppliesTurnNetworkGrant (this branch's rewrite of the existing curl test) exercises it end to end — prompt → approve → turn network grant applied → git push gitlawb://… actually runs — and it passes.

@copilot: use a gitlawb:// URL rather than a bare token — applied earlier in d72a192; the analyzer, risk, and end-to-end tests all use gitlawb://example.com/repo.git. @coderabbitai: t.Errorf over t.Fatalf in loops — applied; the unparseable table runs as subtests with t.Errorf, and the new test does the same.

Validation (Windows host, Go 1.26.5): go build ./..., go vet ./..., go test ./... -count=1 all pass; GOOS=linux|darwin|windows builds clean; gofmt clean on every changed file.

@jatmn @Vasanthdev2004 @gnanam1990 — ready for another look; I can't use the reviewer-request button on this repo, hence the mention.

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

@PierrunoYT I’ll review the updated PR, with particular attention to option parsing, Windows executable normalization, and ensuring the parseable AST path—not only the fail-closed fallback—enforces the network classification.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/sandbox/risk_hardening_test.go (1)

337-342: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Cover git.exe in the unparseable network fallback.

unparseableNetworkPattern matches git only, so an unparseable git.exe push ... misses the critical network category even though parseable git.exe is classified correctly. Accept an optional .exe suffix and add that regression case.

Proposed fix
-|\bgit(?:\s+[^\s;&|]+){0,8}\s+(clone|fetch|pull|push)\b
+|\bgit(?:\.exe)?(?:\s+[^\s;&|]+){0,8}\s+(clone|fetch|pull|push)\b
+ `git.exe push gitlawb://example.com/repo.git main && "unterminated`,

As per coding guidelines, **/*_test.go requires regression tests for behavior changes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/sandbox/risk_hardening_test.go` around lines 337 - 342, Update the
unparseableNetworkPattern to match both git and git.exe command names while
preserving the existing network-command requirements. In the risk-hardening
regression table in the relevant test, add an unparseable git.exe push case so
it is classified under the network category.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@internal/sandbox/risk_hardening_test.go`:
- Around line 337-342: Update the unparseableNetworkPattern to match both git
and git.exe command names while preserving the existing network-command
requirements. In the risk-hardening regression table in the relevant test, add
an unparseable git.exe push case so it is classified under the network category.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3b9a21d6-40d7-4443-869c-0714562d2150

📥 Commits

Reviewing files that changed from the base of the PR and between d72a192 and 595c292.

📒 Files selected for processing (5)
  • internal/agent/loop_test.go
  • internal/sandbox/analyzer.go
  • internal/sandbox/analyzer_test.go
  • internal/sandbox/risk.go
  • internal/sandbox/risk_hardening_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/sandbox/risk.go
  • internal/agent/loop_test.go

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 25, 2026

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found issues that need to be addressed before this is ready.

Findings

  • [P1] Recognize --attr-source as a value-taking Git global option
    internal/sandbox/analyzer.go:308
    Git accepts --attr-source <tree-ish> before the subcommand (for example, git --attr-source HEAD push origin main), but it is absent from this new skip list. gitSubcommand therefore returns head instead of push; because this command parses successfully, the fallback is not consulted and the shell call never receives the critical network classification or its network-enabled approval profile. Add this global option and a regression case; update the paired command-prefix parser too if the documented mirroring is intentional.

  • [P2] Make the unparseable Git fallback cover the supported Windows form and option count
    internal/sandbox/risk.go:36
    The new AST path normalizes git.exe, but parser-failing Windows commands never reach that code. For example, git.exe push origin main & rem ' runs under cmd.exe but is rejected by the POSIX parser; this pattern requires whitespace immediately after git, so classification adds only unparseable_command, not network, and skips the network approval/turn-grant path. The {0,8} cap has the same failure once a Git invocation has more than four value-taking global options. Match an optional .exe suffix and scan Git tokens up to a command separator without the arbitrary cap, with regressions for both forms.

…allback

gitSubcommand (and its mirror in internal/agent/command_prefix.go) was
missing --attr-source from git's value-taking global options, so
`git --attr-source HEAD push origin main` resolved to the wrong subcommand
and never got classified as network access.

The unparseable-command regex fallback used when the shell parser fails now
also matches an optional .exe suffix, so a Windows form like
`git.exe push origin main & rem '` — valid under cmd.exe but rejected by the
POSIX parser AnalyzeCommand uses — still classifies as network. The
generic-token scan before the subcommand no longer caps at 8 tokens; Go's
regexp package is RE2-backed (linear time, no backtracking blowup), so the
cap only served to silently drop coverage once a git invocation had more
than a handful of value-taking global options.

Addresses review feedback from jatmn on PR Gitlawb#726.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/sandbox/risk.go`:
- Around line 36-44: The unparseable Git fallback in unparseableNetworkPattern
must not treat arbitrary tokens before push, fetch, or pull as global options.
Restrict matching to recognized Git global options and their values, or reuse
the shared token-aware resolver from analyzer.go, while preserving support for
git.exe and complex valid invocations. Add a regression test covering a local
command such as git status push so it is not classified as network.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 59e3c8be-fb6a-46bd-8c23-15d475e08ebd

📥 Commits

Reviewing files that changed from the base of the PR and between 595c292 and d54e971.

📒 Files selected for processing (5)
  • internal/agent/command_prefix.go
  • internal/sandbox/analyzer.go
  • internal/sandbox/analyzer_test.go
  • internal/sandbox/risk.go
  • internal/sandbox/risk_hardening_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/sandbox/risk_hardening_test.go
  • internal/sandbox/analyzer.go

Comment thread internal/sandbox/risk.go Outdated
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 26, 2026
@PierrunoYT

Copy link
Copy Markdown
Contributor Author

Final follow-up on current head f5ad5de:

  • the Windows fallback now preserves quoted Git option values and quoted verbs
  • regressions cover paths containing spaces and quoted "push" while retaining the git status push negative case
  • current upstream main was merged normally to include the unrelated Windows test fix
  • full validation and all GitHub checks pass; no active review threads remain

The PR is mergeable. PierrunoYT requests a fresh review to supersede the previous CHANGES_REQUESTED decision.

@PierrunoYT

Copy link
Copy Markdown
Contributor Author

Current upstream main is merged normally in fcc50ab, and the final Windows/parser follow-up is 89c8e38. GitHub now reports the PR as MERGEABLE.

Fresh review-history and thread audit:

  • all earlier AST findings remain covered: value-taking Git globals (including --attr-source), git.exe, parseable -C/-c/--git-dir forms, custom gitlawb:// transport, and the approved-turn network grant
  • the unparseable Windows fallback now preserves separate and joined quoted option values (-C "C:\Program Files\repo", -C"...", --git-dir="...", and a fully quoted inline option), fully quoted Git executable paths, and quoted verbs
  • Windows executable spellings align with current normalizeProgramToken, including git.cmd
  • fallback network verbs now match the AST set: clone|fetch|pull|push|ls-remote|archive
  • negative regressions retain the important boundary: git status push, quoted "status", and push appearing only inside an option value remain non-network
  • all five unresolved threads are outdated Copilot threads whose requests are covered by current code/tests; there are no unresolved non-outdated threads

Validation on Go 1.26.5:

  • focused analyzer/risk/approval/command-prefix tests: pass
  • make fmt-check: pass
  • go vet ./...: pass
  • go test ./... -count=1: pass
  • go run ./cmd/zero-release build: pass
  • go run ./cmd/zero-release smoke: pass
  • make lint-static: pass (0 issues)
  • make vulncheck: pass (no vulnerabilities)
  • GOOS=windows GOARCH=amd64 go build ./...: pass
  • git diff HEAD --check: pass

All current GitHub checks pass, including Ubuntu/macOS/Windows Smoke, Security & code health, Performance Smoke, Zero Review, and CodeRabbit status: CI run, review run.

The only remaining gate is the stale CHANGES_REQUESTED review decision from an earlier commit. @jatmn @Vasanthdev2004 — PierrunoYT requests a fresh review of current head 89c8e38.

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 27, 2026
@PierrunoYT
PierrunoYT requested a review from jatmn July 27, 2026 23:42

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found issues that need to be addressed before this is ready.

Findings

  • [P2] Do not treat -c/-C option values as git subcommands in the unparseable fallback
    internal/sandbox/risk.go:54
    The unparseable git branch can match -c or -C through the generic dash-option alternative without consuming the following token as the option value. When that next token is a network verb (push, fetch, etc.), the regex treats it as the subcommand even though it is only a config key or work-tree path. For example, git -c push origin main & rem ' and git -C push origin main & rem ' classify as critical network on the fallback path, and quoted forms such as git -c "push" origin main & rem ' and git -C "push" status & rem ' do the same. Base main did not classify these as network. This is the same failure mode you fixed for bare tokens like status in git status push, but it still applies when the misleading token is a -c/-C value. Please ensure value-taking globals always consume their value token before subcommand matching (or exclude -c/-C from the generic dash-option arm), and add regressions beside TestClassifyUnparseableNonGitOptionTokenStaysNonNetwork.

  • [P2] Do not match .git URL/path fragments as a git executable in the unparseable fallback
    internal/sandbox/risk.go:54
    The new \bgit(?:\.(?:exe|cmd|bat|com))? program matcher also matches the git suffix inside .git URLs and hostnames. On the unparseable fallback path, strings such as bar.git push origin & rem ' and https://github.com/foo/bar.git push origin && "unterminated classify as critical network even though no git command is present. Base main did not have this behavior. Anchor the matcher to an actual git invocation (for example require whitespace or a path separator before git, not a . from .git) and add negative regressions for .git URLs/pathspecs that happen to contain push/fetch tokens later in the string.

  • [P3] Unparseable fallback now false-positives on git --help <topic> / git --version <topic> forms
    internal/sandbox/risk.go:54
    Commands such as git --help push & rem ' and git --version push & rem ' now classify as critical network on the unparseable fallback path because the generic dash-option skip advances to the topic token and treats it as the subcommand. Base main did not classify these on the fallback path. Please extend the negative coverage to these dash-flag forms, or halt the scan when a valueless global is followed by a bare token that is not a real git subcommand. Note: parseable git --help push was already classified as network on base main via the AST path (firstSubcommand skipping --help); that pre-existing AST behavior is outside this diff and not introduced by the regex change.

@PierrunoYT

Copy link
Copy Markdown
Contributor Author

PierrunoYT addressed all fallback-classification findings in 8a7ffe6.

  • Replaced the fragile Git fallback regex with command-position, token-aware matching.
  • -c/-C consume their values, including quoted values.
  • .git URL/path fragments no longer count as Git executables.
  • Terminal --help/--version forms no longer false-positive.
  • Genuine unparseable Git network commands and Windows executable/path forms remain detected.

Validation: focused risk-classification tests pass with -race; formatting and diff checks pass. Ready for re-review.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/sandbox/analyzer.go (1)

281-283: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Classify git archive as networked only with --remote.

Local git archive HEAD reads local objects and does not require network access, but both network classification paths make archive network-only.

  • internal/sandbox/analyzer.go#L281-L283: require --remote/--remote=… before marking archive as network.
  • internal/sandbox/analyzer_test.go#L89-L90: add a local git archive HEAD regression with network: false.
  • internal/sandbox/risk.go#L100-L281: apply the same --remote condition in fallback token parsing.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/sandbox/analyzer.go` around lines 281 - 283, Classify git archive as
networked only when --remote or --remote=… is present. Update the gitSubcommand
network classification in internal/sandbox/analyzer.go and the fallback token
parsing in internal/sandbox/risk.go; add a regression case in
internal/sandbox/analyzer_test.go for local git archive HEAD expecting network:
false.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@internal/sandbox/analyzer.go`:
- Around line 281-283: Classify git archive as networked only when --remote or
--remote=… is present. Update the gitSubcommand network classification in
internal/sandbox/analyzer.go and the fallback token parsing in
internal/sandbox/risk.go; add a regression case in
internal/sandbox/analyzer_test.go for local git archive HEAD expecting network:
false.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d5e72f2c-787b-4dba-9838-0d095cca0ac3

📥 Commits

Reviewing files that changed from the base of the PR and between f5ad5de and 8a7ffe6.

📒 Files selected for processing (6)
  • internal/agent/command_prefix_test.go
  • internal/agent/loop_test.go
  • internal/sandbox/analyzer.go
  • internal/sandbox/analyzer_test.go
  • internal/sandbox/risk.go
  • internal/sandbox/risk_hardening_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/agent/loop_test.go
  • internal/sandbox/risk_hardening_test.go

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 28, 2026

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found issues that need to be addressed before this is ready.

Findings

  • [P2] Restore network classification for wrapper-prefixed unparseable commands
    internal/sandbox/risk.go:71
    This is a confirmed regression from 8a7ffe6, not pre-existing drift. The refactor of matchesUnparseableNetwork tokenizes each ;/&/| segment and only elevates to network when tokens[0] is a git executable or when the rebuilt segment matches the new ^-anchored unparseableNetworkPattern. That drops base main's whole-string \b(curl|wget|…)\b / \bgit\s+clone\b matching, so obfuscated commands where the network binary is not argv[0] lose the network category. Verified on base vs head: classifyCommand(sudo curl https://example.com && "unterminated) goes from Critical + network to High + unparseable_command only; the same miss hits env curl …, env git fetch …, sudo git push …, sudo npm install …, timeout 5 curl …, PATH=.:$PATH git push …, and sh -c 'curl …' && "unterminated. Engine impact is also verified: with NetworkDeny and PermissionGranted: true, base returns ActionPrompt / ReasonNetworkBlocked, while head returns ActionAllow with "tool requires approval before execution". The parseable AST fixes for git -C repo push, git.exe, and quoted Windows forms are working; the gap is only on the fail-closed fallback when the executable is behind a wrapper or env assignment. Please keep the git-specific token-aware matcher, but also peel wrapperPrograms / env assignments (as effectiveProgram already does on the parseable path) or otherwise restore fail-closed detection on this path, and add regressions beside TestClassifyUnparseableNetworkCommandFailsClosed.

  • [P3] Strip Windows executable suffixes in the unparseable fallback
    internal/sandbox/risk.go:118
    Companion regression from the same matchesUnparseableNetwork refactor. Parseable classification already normalizes curl.exe through normalizeProgramToken, but executableTokenBase only lowercases the basename and leaves .exe attached. Verified: unparseable curl.exe https://example.com && "unterminated loses network on head (network=false) while base main still matched it via \bcurl\b (network=true). Please reuse the same suffix normalization here (or call normalizeProgramToken) and add an unparseable curl.exe/wget.exe regression.

  • [P3] Split fallback segments on newlines
    internal/sandbox/risk.go:129
    Companion regression from the same refactor. fallbackCommandTokens flushes commands only on ;, &, and |, not on \n. Verified: true\ncurl https://example.com && "unterminated stays one segment whose first token is true, so the embedded curl is invisible to the new matcher (network=false on head, network=true on base). Please treat newline as a command separator (or otherwise scan later tokens in the segment) and add a regression.

  • [P3] Gate local git archive on --remote when touching this verb list
    internal/sandbox/analyzer.go:281
    This one is partly drift, not a merge blocker for the core git push fix. Treating every archive subcommand as network is already true on base main for git archive HEAD; this PR's gitSubcommand fix extends that pre-existing false positive to git -C repo archive HEAD, which base misclassified as offline because it read repo as the subcommand. git archive only needs network with --remote. If you are already editing the verb list in this PR, please gate both the AST and fallback matchers on a remote flag and add git archive HEAD / git -C repo archive HEAD negative cases with network: false. If not, this can be a follow-up; it should not block the wrapper-regression fix above.

Reworking the fallback to be token-aware made it read the program from
tokens[0], which is only the program when nothing precedes it. A wrapper prefix,
an environment assignment, or a shell launcher put it elsewhere, so
`sudo curl …`, `env git fetch …`, `timeout 5 curl …`, `PATH=.:$PATH git push …`,
and `sh -c 'curl …'` all lost the network category that the previous
whole-string match caught. Under NetworkDeny with permission already granted
that turned a network prompt into a plain allow — egress granted to a command
too obfuscated to parse.

The fallback now resolves each segment's program exactly as the parseable path
does, through commandBodyFields (extracted from commandBody), and recurses into
a shell launcher's -c payload the way analyzeInto does. Resolving the program
rather than matching a network name anywhere in the string is what still keeps
`git status push` and `echo https://example.com/repo.git push` out.

Two companion gaps from the same refactor: executableTokenBase now strips
Windows executable suffixes through the same helper normalizeProgramToken uses,
so `curl.exe` matches here as it does there, and the fallback tokenizer treats a
newline as a command separator rather than as whitespace, so a program on a
later line is resolved instead of being read as arguments of the first.

Separately, `git archive` is now gated on --remote on both paths. A local
archive streams the object store and never leaves the machine; treating every
archive as network cost a proactive prompt on an offline command, and this PR's
gitSubcommand fix had extended that false positive to `git -C repo archive`.

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

Copy link
Copy Markdown
Contributor Author

Addressed all four findings in 6225826. Thanks for the base-vs-head verification — it pinned the regression precisely.

[P2] Wrapper-prefixed unparseable commands are fail-closed again. You called it: reading the program from tokens[0] is only correct when nothing precedes it. The fallback now resolves each segment's program the same way the parseable path does, and I did it by reusing that path's own scan rather than writing a third copy — commandBody was already doing exactly this walk (env assignments → wrapper prefixes → wrapper-consumed option values), so I extracted commandBodyFields from it and the fallback calls that. sh -c '…' needed one more thing: the fallback tokenizer keeps a quoted payload as a single token, so the program inside it is not a token of the segment at all — that recurses now, bounded by maxUnparseableShellDepth, mirroring analyzeInto.

Resolving the program instead of reverting to whole-string matching is what still keeps git status push and echo https://example.com/repo.git push non-network; that whole negative suite passes untouched.

[P3] curl.exe. executableTokenBase now strips the Windows suffixes through a shared trimExecutableSuffix/executableSuffixes that normalizeProgramToken also uses, so the two paths can't normalize differently again — which was the actual mechanism here. isGitExecutableToken's hardcoded git.exe/git.cmd/… list became redundant and is gone.

[P3] Newlines. fallbackCommandTokens now flushes a command on \n/\r instead of treating them as whitespace.

[P3] git archive. Gated on --remote on both paths (gitTargetsRemoteArchive, matching joined and separated spellings), since you were already asking me to touch the verb list. git archive HEAD and git -C repo archive HEAD are now network: false; git archive --remote=origin HEAD stays true.

Regressions (each verified to fail with the source changes stashed and the tests kept):

  • TestClassifyUnparseableNetworkBehindWrapperFailsClosed — 17 cases covering your exact list plus sudo -u root curl, xargs curl, GIT_SSH_COMMAND=… git push, bash -c "git push …", wget.exe, and CRLF. All 17 fail before the fix.
  • TestClassifyUnparseableLocalGitArchiveStaysNonNetwork — 4 local-archive cases, all failing before (they were network).
  • TestEvaluatePromptsForUnparseableNetworkBehindWrapper — the engine-level assertion for the impact you measured: NetworkDeny + PermissionGranted: true must yield ActionPrompt/ReasonNetworkBlocked. Fails before the fix with ActionAllow, which is the behavior that actually matters here.
  • Analyzer table: local-archive negatives plus a separated-value --remote positive.

Validation

  • go build ./..., go vet ./..., go test ./...
  • go test ./internal/sandbox -count=1
  • Each new test confirmed failing against this branch's pre-fix source
  • git diff --check clean; gofmt -l clean for the touched files (five files in internal/sandbox are already flagged on this branch and I left them alone)

@PierrunoYT
PierrunoYT requested a review from jatmn July 28, 2026 20:22

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/sandbox/analyzer.go`:
- Around line 300-303: Update the argument scan in the analyzer logic to stop
processing words once the standalone `--` separator is encountered, so
subsequent pathspecs such as `--remote` are not classified as network options.
Preserve detection of `--remote` and `--remote=...` before the separator, and
add a regression case covering `git archive HEAD -- --remote`.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: c0ce84d3-2a63-4901-b4ef-1978ea27905e

📥 Commits

Reviewing files that changed from the base of the PR and between 8a7ffe6 and 6225826.

📒 Files selected for processing (6)
  • internal/sandbox/analyzer.go
  • internal/sandbox/analyzer_test.go
  • internal/sandbox/engine_test.go
  • internal/sandbox/risk.go
  • internal/sandbox/risk_hardening_test.go
  • internal/sandbox/safe_command.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • internal/sandbox/risk.go

Comment thread internal/sandbox/analyzer.go

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found issues that need to be addressed before this is ready.

Findings

  • [P2] Respect -- when scanning git archive for --remote
    internal/sandbox/analyzer.go:299
    gitTargetsRemoteArchive walks every argument token and treats any --remote / --remote=… as a network archive, with no stop at the standalone -- end-of-options marker. After --, operands are pathspecs, not options, so git archive HEAD -- --remote (and the same shape with -C / -o) is a local archive of a tree entry named --remote, not a remote fetch. Both the parseable AST path (gitUsesNetwork) and the unparseable fallback (matchesUnparseableGitNetwork) call this helper, so the false positive hits proactive network prompts and turn network grants for a purely local command. This gap was introduced when this PR gated archive on --remote; it is not pre-existing drift. The same package already handles -- correctly for rm in hasRecursiveForce; mirror that pattern here and add a regression for git archive HEAD -- --remote expecting network: false. CodeRabbit's CHANGES_REQUESTED review on head 6225826 is still open on this point.

  • [P3] Align unparseable shell recursion depth with the AST path
    internal/sandbox/risk.go:74
    maxUnparseableShellDepth is 3 while maxAnalyzerDepth in analyzer.go is 4, despite the comment that the fallback "mirrors" the parseable limit. On a fourth nested sh -c / bash -c layer that is also unparseable, the AST path can still reach an inner curl / git push, but matchesUnparseableNetworkAt stops recursing one level earlier and drops the network category. Under NetworkDeny with shell permission already granted, that is the same fail-open shape the wrapper-prefix regression fixed in 6225826, but it requires heavy obfuscation and is unlikely in normal use. This mismatch is new in this PR's fallback code, not drift from base main. Either bump the fallback cap to 4 or document and test the intentional difference.

Follow-up (optional for #703; pre-existing or edge-case parity)

These are real classification gaps, but they are mostly outside the core git push fix or require obscure spellings. I would not block merge on them alone once the git archive -- issue above is fixed.

  • [P3] Recurse into --command payloads on launcher paths
    internal/sandbox/risk.go:152 and internal/sandbox/analyzer.go:488
    fallbackDashCPayload and dashCPayload only recognize -c, while shellDashCPayload in safe_command.go already accepts both -c and --command. An unparseable bash --command "git push origin main" && "unterminated never recurses into the payload, so classification can stay at unparseable_command without network after a prior shell approval. The -c-only limitation on the parseable AST path predates this PR; this branch copied it into the new fallback instead of sharing shellDashCPayload. Worth fixing if you touch launcher recursion, but it is parity drift rather than a new regression from the 6225826 wrapper fix.

  • [P3] Normalize drive-relative Windows executables on the fallback path
    internal/sandbox/risk.go:166
    executableTokenBase strips basename and suffixes but does not call windowsExecutablePathBasename, so drive-relative spellings such as C:git.exe normalize to c:git instead of git. The parseable path already classifies 'C:curl.exe' https://example.com as network via normalizeProgramToken. An unparseable Windows form like C:git.exe push origin main & rem ' can miss the network category on the fail-closed path while the AST path would catch it. This is an obscure Windows spelling gap on the fallback path introduced when this PR added executableTokenBase parity claims; the common git.exe / quoted-path forms you already test are fine.

Rechecked — prior review items look addressed on current head

The wrapper-prefix, curl.exe, newline-segment, and local-git archive regressions from the 8a7ffe6 review appear fixed in 6225826, with matching coverage in TestClassifyUnparseableNetworkBehindWrapperFailsClosed, TestEvaluatePromptsForUnparseableNetworkBehindWrapper, and TestClassifyUnparseableLocalGitArchiveStaysNonNetwork. Parseable -C / -c / --git-dir / git.exe / --attr-source forms and the git status push negatives also still look covered. Windows Smoke failed on internal/tools TestExecCommandForegroundServerReturnsSessionAndServesHTTP; this PR does not touch internal/tools, and internal/sandbox and internal/agent passed on that CI run.

Gating `git archive` on --remote scanned every token, with no stop at the
standalone `--`. After that separator the operands are pathspecs, so
`git archive HEAD -- --remote` archives a tree entry NAMED --remote out of the
local object store — and was classified as network, costing a proactive prompt
and a network grant on a purely local command. Both the AST path and the
unparseable fallback call the same helper, so both were wrong; the end-of-options
rule is the one hasRecursiveForce already applies to `rm -- -rf`.

Three parity gaps in the same code, each one a claim my own comments made:

The fallback's launcher recursion cap said it mirrored the parseable path while
being one level shallower, so a fourth nested layer that the analyzer still
reaches would drop the network category. It now IS maxAnalyzerDepth rather than
a copy of its value.

Both payload scanners knew only `-c`, though bash and zsh accept `--command` and
shellDashCPayload already handled both — so `bash --command 'git push …'` was
never recursed into on either path.

executableTokenBase claimed parity with normalizeProgramToken but skipped
windowsExecutablePathBasename, leaving a drive-relative `C:git.exe` as "c:git"
and unmatched on the fail-closed path while the AST path classified it.

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

Copy link
Copy Markdown
Contributor Author

All four addressed in 0b56cd6 — the P2 and both P3s, plus both optional parity items, since three of the four were claims my own comments made.

[P2] -- ends the option scan. You and CodeRabbit are both right, and its repro script settles it: git archive HEAD -- --remote archives a tree entry named --remote from the local object store. gitTargetsRemoteArchive now stops at the separator, mirroring what hasRecursiveForce already does for rm -- -rf. Since both the AST path and the fallback call that helper, one fix covers both. Regressions added on both paths, including a positive git archive --remote=origin HEAD -- src so the stop does not swallow a real remote.

[P3] Recursion depth. Bumped, but by making it maxUnparseableShellDepth = maxAnalyzerDepth rather than writing 4 — the two drifted precisely because one was a copy of the other's value, and my comment claimed they mirrored while they did not.

On testing it: I could not honestly drive a four-layer sh -c chain through the fallback. That tokenizer is deliberately small and does not model nested escaped quotes, so a literal four-deep string tests the escaping rather than the depth limit — my first attempt failed for exactly that reason. So the depth relationship is pinned as a constants assertion with that reasoning written down, and a two-layer nested case covers that recursion happens at all. Happy to do it differently if you would rather see a behavioral test.

[P3, optional] --command payloads. Taken. Both dashCPayload and fallbackDashCPayload now accept -c and --command through a shared predicate, so bash --command 'git push …' is recursed into on both paths.

[P3, optional] Drive-relative Windows executables. Taken. executableTokenBase now goes through windowsExecutablePathBasename before the separator scan, so C:git.exe normalizes to git instead of c:git. My comment on that function asserted parity with normalizeProgramToken; it now has it.

Tests: 11 new cases across TestAnalyzeCommand, TestClassifyUnparseableNetworkBehindWrapperFailsClosed, and TestClassifyUnparseableLocalGitArchiveStaysNonNetwork, plus TestUnparseableShellDepthMatchesAnalyzerDepth. I reverted all four fixes at once and confirmed every new case fails, so none of them are passing by accident.

Validation: go build ./... plus GOOS=linux/GOOS=darwin, go vet ./..., go test ./..., git diff --check and gofmt -l clean.

Agreed the Windows Smoke failure is unrelated — TestExecCommandForegroundServerReturnsSessionAndServesHTTP is in internal/tools, which this diff does not touch.

@PierrunoYT
PierrunoYT requested a review from jatmn July 29, 2026 12:42

@jatmn jatmn left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

sandbox network grants don't persist after approving — network access stays denied, fails with context canceled

6 participants