Skip to content

Fix #477: block interpreter-mediated writes in builder guard and validator sandbox - #488

Merged
richard-devbot merged 9 commits into
mainfrom
fix-477-interpreter-mediated-writes
Jul 30, 2026
Merged

Fix #477: block interpreter-mediated writes in builder guard and validator sandbox#488
richard-devbot merged 9 commits into
mainfrom
fix-477-interpreter-mediated-writes

Conversation

@richard-devbot

@richard-devbot richard-devbot commented Jul 30, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #477 (P0, Wave 0 of the #476 autonomy-hardening epic). Two independent audits verified the same live bypass: node -e/python3 -c/perl -e/etc. write directly to protected .rstack/ governance state and source files, invisible to both the builder guard and the (supposedly stricter, deny-outright) validator sandbox — neither classifier modeled general-purpose interpreters as an arbitrary read/write/exec capability, only shell verbs and redirects.

  • destructive-actions.js: new isInterpreterExecCommand() detects inline-eval invocation (node -e/--eval/-p, python(2/3) -c, perl -e/-E, ruby -e, php -r, osascript -e, pwsh/powershell -Command/-EncodedCommand, deno eval) and bare-interpreter/piped-stdin invocation with no script argument (functionally identical to -e). Deliberately does not try to parse the inline code itself — the mere ability to evaluate arbitrary code inline is the capability being classified. A plain node script.js / python3 manage.py runserver (a committed, on-disk file) stays allowed. Wired into classifyCommand as a new INTERPRETER_EXEC category — gateable via the existing per-task destructive-action:<taskId> approval with no new guard.js wiring needed, since approval-gating is category-agnostic — and into commandWritesFile (so the [P1][harness] Attempt/telemetry budgets are Pi-only — guard must enforce budgets on bridge/guard harnesses #373 BLOCKED-task gate can't be bypassed the same way).
  • validator-sandbox.js: reuses the exact same detector (one source of truth for the classification, even though builder/validator apply different policies to it) as a new interpreter-exec deny rule — no approval escape hatch, consistent with every other rule.
  • Two smaller gaps found during audit cross-validation, fixed alongside since they're in the same files/blast radius:
    • guard --explain always exited 0 even for a block decision — a hook integration checking only the exit code would fail open on an explained block. Exit code now matches the real decision in both explain branches.
    • PROTECTED_CONFIG_PATTERN matched only the two literal filenames (settings.json/settings.local.json), not the true .claude/settings*.json glob Claude Code actually supports (e.g. settings.project.json). Broadened to a real glob.
  • rstack-sdlc.ts: a delegated subprocess's env no longer inherits RSTACK_ALLOW_DESTRUCTIVE from the parent process — an operator's own scoped override was silently widening to every spawned sub-delegate.

Test plan

Scope notes (read before merge)

  • Heredoc-into-interpreter (node <<EOF) is a known residual gap — the bare-interpreter check catches piped stdin with zero arguments, but a heredoc marker counts as an "argument" in the simple tokenizer, so it isn't caught. Not in the audits' concrete PoCs; flagged here for transparency rather than silently claimed as covered.
  • python -m <module> (running a named, already-installed module) is intentionally not flagged — it takes no inline code argument, unlike -c, and is extremely common legitimate usage (python -m pytest, python -m http.server).
  • The guard's "empty/unclassifiable input fails open" policy (documented, deliberate, in guard.js's header comment) is unchanged — that's a narrower, distinct corner case (literally nothing to classify) from the interpreter-classification gap this PR closes.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Security Improvements
    • Blocked interpreter one-liners (inline code / stdin eval) from performing hidden file changes or arbitrary execution.
    • Expanded protection for sensitive configuration paths and ensured delegated processes can’t inherit destructive-operation overrides.
    • Tightened dependency vulnerability handling for bundled packages with expanded tolerated advisory allowlisting.
  • Bug Fixes
    • Fixed --explain/sandbox exit codes to match blocking decisions and avoid fail-open behavior.
  • Documentation
    • Updated destructive-action guidance with the new interpreter category and additional protected config paths.
  • Tests
    • Added/expanded coverage for interpreter detection, delegated environments, and security-audit decisions.

richardsongunde and others added 3 commits July 26, 2026 17:21
…#472)

npm audit reported 5 high-severity advisories, all tracing to one root:
a DoS/ReDoS bug in brace-expansion (GHSA-3jxr-9vmj-r5cp,
GHSA-mh99-v99m-4gvg), reachable via eslint -> minimatch@3.1.5 ->
brace-expansion@1.1.16. npm's only reported fix was eslint@10
(isSemVerMajor), which pulls in a new no-useless-assignment rule
in @eslint/js's recommended config, flagging 15 pre-existing
dead-store patterns across src/ -- too invasive for a security-only
fix.

Instead: add a root `overrides` entry (this repo already uses the
same mechanism for esbuild/protobufjs/ws) forcing brace-expansion to
the patched 5.0.8 line. Verified this reaches eslint's own tree
(minimatch@3.1.5's brace-expansion now 5.0.8) while, as documented,
leaving @earendil-works/pi-coding-agent's shrinkwrapped copy (5.0.6)
untouched -- npm honors a shrinkwrap absolutely, so overrides can't
reach it regardless.

That shrinkwrapped copy is the only remaining high-severity finding,
and it's exactly the node this gate's BUNDLED_ROOTS/
TOLERATED_BUNDLED_ADVISORIES allowlist already exists to tolerate --
added the second GHSA id to that allowlist (the gate is deliberately
strict per-advisory-id, so a newly surfaced GHSA for an
already-tolerated package still fails until consciously added).

Verified: npm run lint unchanged (0 errors, same 4 pre-existing
warnings -- confirms minimatch@3.1.5 works fine with the newer
brace-expansion at runtime), npm run typecheck clean, gate logic
simulated against real npm audit output now reports 0 blocking.
Qodo flagged four issues on the eslint/brace-expansion security fix:
- no test coverage for the TOLERATED_BUNDLED_ADVISORIES allowlist change
- brace-expansion@5.0.8 declares engines.node "20 || >=22", excluding the
  Node 18 CI lane
- the ">=5.0.8" override is unbounded and can float to a future major
- the script comment mis-attributed the version bump to an eslint 9->10
  bump that never happened (the real mechanism is the root override)

Fix: bound the override to an exact "5.0.8" pin; export the pure
tolerate/block decision logic from security-audit.mjs (evaluateAdvisories,
isFullyBundled, advisoryIds) behind a main-guard so it's unit-testable
without shelling out to npm audit, and add tests/security-audit.test.js
covering the tolerate/block/allowlist-miss/severity-threshold paths, plus
a pinned-allowlist regression test; correct the stale comment to document
the real mechanism and the accepted Node 18 engines trade-off (no
brace-expansion release fixes both GHSAs while keeping Node 18 support;
it's a devDependency-only path and no engine-strict is set, so this
doesn't fail CI in practice).

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

Two independent audits verified the same live bypass: node -e/python3 -c/
perl -e/etc. can write directly to protected .rstack/ governance state and
source files, invisible to both the builder guard and the (supposedly
stricter, deny-outright) validator sandbox — neither classifier modeled
general-purpose interpreters as an arbitrary read/write/exec capability,
only shell verbs and redirects.

- destructive-actions.js: new isInterpreterExecCommand() detects inline-eval
  invocation (node -e/--eval/-p, python/python2/python3 -c, perl -e/-E,
  ruby -e, php -r, osascript -e, pwsh/powershell -Command/-EncodedCommand,
  deno eval) and bare-interpreter/piped-stdin invocation with no script
  argument (functionally identical to -e — reads its program from stdin).
  Deliberately does NOT try to parse the inline code itself (undecidable in
  general) — the mere ability to evaluate arbitrary code inline is the
  capability being classified, same as every other DESTRUCTIVE_CATEGORIES
  entry. A plain `node script.js` / `python3 manage.py runserver` (running a
  committed, on-disk file) stays allowed. Wired into classifyCommand as a new
  INTERPRETER_EXEC category (gateable via the existing per-task
  destructive-action:<taskId> approval — no new guard.js wiring needed, since
  approval-gating is category-agnostic) and into commandWritesFile (so the
  #373 BLOCKED-task gate can't be bypassed the same way).
- validator-sandbox.js: reuses the exact same detector (one source of truth
  for the classification itself, even though builder/validator apply
  different policies to it) as a new interpreter-exec deny rule — no approval
  escape hatch, consistent with every other rule in that list.
- Two smaller gaps found during the audit cross-validation, fixed alongside:
  `guard --explain` always exited 0 even for a `block` decision (a hook
  integration checking only the exit code would fail open on an explained
  block) — the exit code now matches the real decision in both the builder
  and validator-sandbox explain branches. `PROTECTED_CONFIG_PATTERN` matched
  only the two literal filenames `.claude/settings.json`/`settings.local.json`
  rather than the true `settings*.json` glob Claude Code actually supports
  (e.g. `settings.project.json`) — broadened to a real glob.
- rstack-sdlc.ts: a delegated subprocess's env no longer inherits
  RSTACK_ALLOW_DESTRUCTIVE from the parent process — an operator's own
  scoped override was silently widening to every spawned sub-delegate.

Adversarial test coverage added across all four surfaces: the classifier
(harness-destructive-actions.test.js), the validator sandbox
(harness-validator-sandbox.test.js), the guard CLI end-to-end including the
#373 x #477 interaction (guard-cli.test.js), and the delegate env-scrub
(harness-validator-sandbox-hook.test.js). 1636/1636 tests, typecheck clean,
lint clean, security-audit clean.

Fixes #477.

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

strix-security Bot commented Jul 30, 2026

Copy link
Copy Markdown

Strix Security Review

1 open security finding on this PR:

5 resolved findings
Review summary

Reviewed all security-relevant changes in the PR, including the new interpreter-exec classifier, validator sandbox reuse, guard exit-code handling, delegated environment scrubbing, and the security-audit allowlist logic. The previously reported glued env --split-string=<cmd> bypass is fixed on the current head and was confirmed resolved. One new issue remains in the updated interpreter detector: cmd /c wrappers are not unwrapped before capability analysis, so interpreter one-liners can still evade the new guard and the validator’s read-only denial when launched through Windows cmd.

Fixed the findings? re-run the review, or tag @strix-security in a PR comment to run a fresh review.

Updated for 775758a.


Reviewed by Strix
Re-run review · Configure security review settings

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@richard-devbot, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 50 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: bff4176f-267e-49c7-98b0-c03fae83c847

📥 Commits

Reviewing files that changed from the base of the PR and between a1cec3d and 775758a.

📒 Files selected for processing (2)
  • src/core/harness/destructive-actions.js
  • tests/harness-destructive-actions.test.js
📝 Walkthrough

Walkthrough

This PR classifies interpreter-based inline or stdin execution as destructive, blocks it in validator and guard flows, prevents delegated override inheritance, fixes explain-mode exit codes, and extracts security-audit evaluation helpers. It also adds a brace-expansion dependency override and regression tests.

Changes

Security enforcement and audit

Layer / File(s) Summary
Interpreter execution classification
src/core/harness/destructive-actions.js, tests/harness-destructive-actions.test.js, docs/HARNESS.md
Interpreter inline-evaluation and stdin commands receive the interpreter-exec destructive category, count as file-writing capabilities, and recognize settings*.json variants.
Validator and guard enforcement
src/core/harness/validator-sandbox.js, src/commands/guard.js, tests/guard-cli.test.js, tests/harness-validator-sandbox.test.js
Validator denial rules detect interpreter execution, while explain-mode exit codes match blocking verdicts and approval/attempt-budget behavior is tested.
Delegated environment isolation
src/integrations/pi/rstack-sdlc.ts, tests/harness-validator-sandbox-hook.test.js
Delegated Pi processes omit RSTACK_ALLOW_DESTRUCTIVE, with validator and builder environment assertions.
Security-audit evaluation
package.json, scripts/security-audit.mjs, tests/security-audit.test.js
Advisory evaluation helpers and allowlists are exported and tested, and brace-expansion is overridden to version 5.0.8.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Agent
  participant GuardCLI
  participant DestructiveClassifier
  participant ValidatorSandbox
  Agent->>GuardCLI: submit interpreter command
  GuardCLI->>DestructiveClassifier: classify command
  DestructiveClassifier-->>GuardCLI: interpreter-exec destructive result
  GuardCLI->>ValidatorSandbox: evaluate guarded action
  ValidatorSandbox-->>GuardCLI: blocking decision
  GuardCLI-->>Agent: block verdict and exit code
Loading

Possibly related PRs

Suggested labels: security, harness, guardrails, testing, priority-p1

Suggested reviewers: richardsongunde

Poem

I’m a rabbit guarding code tonight,
Interpreter tricks meet blockers bright.
Exit codes now tell the truth,
Audit helpers check each proof.
Safe delegates hop away—
No override escapes today!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning package.json and security-audit.mjs add brace-expansion override/allowlist changes that are unrelated to the interpreter-write fix. Split the dependency override and security-audit allowlist work into a separate PR unless it is required for #477.
Docstring Coverage ⚠️ Warning Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Clear, specific title matches the PR's main security fix.
Linked Issues check ✅ Passed The guard, validator sandbox, destructive-action detection, and delegated-env protections align with #477's requirements.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-477-interpreter-mediated-writes

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.

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Fix #477: block interpreter-mediated writes in builder guard and validator sandbox

🐞 Bug fix 🧪 Tests ✨ Enhancement 🕐 40+ Minutes

Grey Divider

AI Description

• Add isInterpreterExecCommand() to detect inline-eval/bare-stdin interpreter invocations
 (node/python/perl/ruby/php/osascript/pwsh/deno) that bypassed shell-verb/redirect classification.
• Wire the new INTERPRETER_EXEC category into builder guard's
 classifyCommand/commandWritesFile and into the validator sandbox as a deny-outright rule reusing
 the same detector.
• Fix --explain always exiting 0 regardless of decision, and broaden PROTECTED_CONFIG_PATTERN to
 a true .claude/settings*.json glob.
• Stop delegate subprocesses from inheriting RSTACK_ALLOW_DESTRUCTIVE from the parent env.
• Refactor scripts/security-audit.mjs into pure, unit-testable functions and add a second
 tolerated brace-expansion GHSA plus an overrides pin to 5.0.8.
• Add extensive adversarial test coverage across guard CLI, harness destructive-actions, validator
 sandbox, sandbox hook, and the security-audit gate.
Diagram

graph TD
  CMD["Shell Command"] --> DETECT["isInterpreterExecCommand()"]
  DETECT --> CLASSIFY["classifyCommand()"] --> GUARD["Builder Guard"]
  GUARD -->|approval exists| ALLOW["Allow"]
  GUARD -->|no approval| BLOCK1["Block exit 2"]
  DETECT --> SANDBOX["Validator Sandbox Rules"] --> BLOCK2["Deny outright"]
  DELEGATE["runDelegateAgent()"] -->|strips RSTACK_ALLOW_DESTRUCTIVE| CHILD["Delegated Subprocess Env"]
  subgraph Legend
    direction LR
    _proc([Process/Function]) ~~~ _term{{Terminal Outcome}}
  end
Loading
High-Level Assessment

The chosen approach — classifying interpreter invocations by their inline-eval/stdin capability rather than attempting to parse the inline code — is the correct and only tractable strategy, since statically determining whether arbitrary inline code writes a file is undecidable in general. Sharing one detector between the builder guard (approval-gated) and validator sandbox (deny-outright) avoids duplicated, divergent classification logic, which was the root cause of the original bypass. No meaningfully better alternative exists for this problem.

Files changed (12) +506 / -66

Enhancement (1) +98 / -1
destructive-actions.jsAdd interpreter-exec detector and INTERPRETER_EXEC category +98/-1

Add interpreter-exec detector and INTERPRETER_EXEC category

• Introduces isInterpreterExecCommand() to detect inline-eval or bare-stdin interpreter invocations (node/python/perl/ruby/php/osascript/pwsh/deno) as a new INTERPRETER_EXEC destructive category wired into classifyCommand and commandWritesFile. Also broadens PROTECTED_CONFIG_PATTERN to a true .claude/settings*.json glob.

src/core/harness/destructive-actions.js

Bug fix (3) +40 / -3
validator-sandbox.jsDeny interpreter-mediated writes outright in validator sandbox +19/-1

Deny interpreter-mediated writes outright in validator sandbox

• Adds a new VALIDATOR_DENIED_COMMAND_RULES entry using a 'test' predicate reusing isInterpreterExecCommand from destructive-actions.js, with no approval escape hatch, and updates matchValidatorDeniedCommand to support predicate-based rules.

src/core/harness/validator-sandbox.js

guard.jsFix --explain exit code to match real decision +13/-2

Fix --explain exit code to match real decision

• Both explain branches (validator-sandbox and destructive-action) now return EXIT_BLOCK when the underlying decision is a block, instead of unconditionally returning EXIT_ALLOW.

src/commands/guard.js

rstack-sdlc.tsStrip RSTACK_ALLOW_DESTRUCTIVE from delegated subprocess env +8/-0

Strip RSTACK_ALLOW_DESTRUCTIVE from delegated subprocess env

• runDelegateAgent no longer propagates the parent process's RSTACK_ALLOW_DESTRUCTIVE override into a spawned delegate's environment, preventing scope-widening of a destructive-approval bypass.

src/integrations/pi/rstack-sdlc.ts

Refactor (1) +62 / -36
security-audit.mjsRefactor security-audit gate into testable pure functions +62/-36

Refactor security-audit gate into testable pure functions

• Extracts evaluateAdvisories, isFullyBundled, advisoryIds, and constants as exported functions/values, wraps process.exit-driven logic in main() gated by an entrypoint check, and adds a second tolerated brace-expansion GHSA.

scripts/security-audit.mjs

Tests (5) +289 / -2
guard-cli.test.jsAdd guard CLI tests for interpreter-exec gating and explain exit codes +56/-2

Add guard CLI tests for interpreter-exec gating and explain exit codes

• New tests cover builder approval-gating of interpreter-exec commands, validator/reviewer denial without approval bypass, BLOCKED-task budget gate interaction, and corrected --explain exit codes.

tests/guard-cli.test.js

harness-destructive-actions.test.jsAdd adversarial corpus for isInterpreterExecCommand and commandWritesFile +75/-0

Add adversarial corpus for isInterpreterExecCommand and commandWritesFile

• Tests every supported interpreter's inline-eval and bare/piped-stdin forms, the exact audit probes, safe-script-file non-flagging, commandWritesFile behavior, and the broadened PROTECTED_CONFIG_PATTERN glob.

tests/harness-destructive-actions.test.js

harness-validator-sandbox.test.jsAdd validator sandbox tests for interpreter-exec denial +39/-0

Add validator sandbox tests for interpreter-exec denial

• Tests that interpreter-mediated writes are denied outright in validator context while committed script-file invocations remain allowed.

tests/harness-validator-sandbox.test.js

harness-validator-sandbox-hook.test.jsAdd RSTACK_ALLOW_DESTRUCTIVE non-inheritance assertions +7/-0

Add RSTACK_ALLOW_DESTRUCTIVE non-inheritance assertions

• Extends the Pi tool_call hook integration test to assert that both validator and builder delegate children never inherit RSTACK_ALLOW_DESTRUCTIVE from the parent process.

tests/harness-validator-sandbox-hook.test.js

security-audit.test.jsAdd unit tests for the refactored security-audit gate +112/-0

Add unit tests for the refactored security-audit gate

• New test file exercising evaluateAdvisories, isFullyBundled, and advisoryIds against synthetic npm audit shapes, and pinning the real TOLERATED_BUNDLED_ADVISORIES allowlist contents.

tests/security-audit.test.js

Other (2) +17 / -24
package.jsonAdd brace-expansion override pin +2/-1

Add brace-expansion override pin

• Adds a brace-expansion 5.0.8 override entry to force the patched line across the non-shrinkwrapped dependency tree.

package.json

package-lock.jsonUpdate lockfile for brace-expansion/balanced-match bump and peer flag cleanup +15/-23

Update lockfile for brace-expansion/balanced-match bump and peer flag cleanup

• Regenerates lockfile entries reflecting brace-expansion 5.0.8 and balanced-match 4.0.4, removes stale peer:true flags, and fixes the pi-ai bin path.

package-lock.json

@qodo-code-review

qodo-code-review Bot commented Jul 30, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📜 Skill insights (3)

Context used
✅ Compliance rules (platform): 300 rules
✅ Skills: 17 invoked
  code-review-pr
  claude-api
  documentation-writing
  pptx
  docx
  performance-monitoring
  security-compliance
  cso
  plan-eng-review
  design-review
  prompt-engineering
  mcp-builder
  qa-testing
  code-patterns
  xlsx
  security-owasp
  testing-qa

Grey Divider


Action required

1. Interpreter detector bypassable ✓ Resolved 🐞 Bug ⛨ Security
Description
isInterpreterExecCommand() only recognizes an interpreter when the *first* whitespace token is
exactly a known verb and eval flags are standalone tokens, so common forms like env node -e …,
sudo python3 -c …, python3.11 -c …, or perl -e'code' won’t be detected and can still write to
protected paths. Because validator-sandbox.js denies interpreter execution using this same
predicate, the same missed forms would also slip through the validator/reviewer/security sandbox
rules.
Code

src/core/harness/destructive-actions.js[R173-195]

+function interpreterVerbAndArgs(segment) {
+  const tokens = segment.trim().split(/\s+/).filter(Boolean).map(stripQuotes);
+  if (!tokens.length) return null;
+  const verb = tokens[0].toLowerCase().replace(/.*[/\\]/, ''); // basename of /usr/bin/node etc.
+  return INTERPRETER_VERBS.has(verb) ? { verb, args: tokens.slice(1) } : null;
+}
+
+// True when the invocation can evaluate arbitrary code inline (`-e`/`-c`/…)
+// or, having no other argument at all, will read its program from stdin
+// (`echo payload | node`, `node <<EOF`) — functionally identical to `-e`.
+// Any OTHER argument (a script path, an unrelated flag) means the interpreter
+// is running a fixed, on-disk file, which is ordinary, lower-risk work.
+function interpreterHasEvalCapability(verb, args) {
+  if (verb === 'deno') return String(args[0] || '').toLowerCase() === 'eval';
+  if (!args.length) return true; // bare interpreter — reads code from stdin
+  const flagSet = INTERPRETER_EVAL_FLAGS[verb];
+  if (!flagSet) return false;
+  const lowerFlags = new Set(flagSet.map((f) => f.toLowerCase()));
+  return args.some((raw) => {
+    const lower = stripQuotes(raw).toLowerCase();
+    return lowerFlags.has(lower) || lowerFlags.has(lower.split('=')[0]);
+  });
+}
Relevance

●●● Strong

Security bypass hardening in harness/sandbox code has strong acceptance precedent.

PR-#399
PR-#380

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The detector requires the first token to be in INTERPRETER_VERBS and only matches eval flags when
they equal the whole token (or flag=value), while stripQuotes() only removes surrounding quotes
and won’t turn -e'code' into -e. The validator sandbox imports this predicate and uses it as a
deny rule, so any false-negative in the detector becomes a sandbox false-negative too.

src/core/harness/destructive-actions.js[155-195]
src/core/harness/destructive-actions.js[173-178]
src/core/harness/destructive-actions.js[185-195]
src/core/harness/destructive-actions.js[273-275]
src/core/harness/validator-sandbox.js[12-88]
src/core/harness/validator-sandbox.js[155-162]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new `isInterpreterExecCommand()` detector can be bypassed by common interpreter invocation forms (wrappers like `env`/`sudo`, version-suffixed interpreter binaries like `python3.11`, and attached short flags like `perl -e'code'`), which undermines the #477 protection in both the builder guard and validator sandbox.

## Issue Context
The detector currently:
- Assumes the interpreter verb is the first whitespace-delimited token.
- Matches eval capability only when an eval flag is an exact token (or `--flag=value`), and treats only zero-arg invocations as stdin-fed.
Validator sandbox denial reuses this same predicate, so its blind spots become sandbox blind spots.

## Fix Focus Areas
- src/core/harness/destructive-actions.js[155-211]
- src/core/harness/validator-sandbox.js[12-88]

## Suggested fix approach
1. **Normalize and locate the interpreter verb within a segment**:
  - Skip common wrappers and prefixes (at minimum: leading env var assignments like `FOO=1`, and wrapper commands like `env`, `command`, `sudo`).
  - Normalize verb basename further by stripping common executable suffixes (e.g. `.exe`) and recognizing version suffix patterns (e.g. `python3.11`, `python3.12`).
2. **Recognize eval flags when attached**:
  - Treat tokens that start with an eval flag as a match (e.g. `-e'code'`, `-c"code"`, `-Eprint`).
3. **Fail closed when no script/module is provided**:
  - If, after consuming flags (and their arguments where applicable), there is no positional script/module argument, treat it as stdin/REPL and therefore `interpreter-exec` (optionally allow a small safe list like `--version`/`--help`).
4. **Add unit tests for bypass forms**:
  - `env node -e "1"`
  - `sudo python3.11 -c "print(1)"`
  - `perl -e'print 1'`
  - `python3 -q` (or similar “no script but has flags”)
  - Windows-style `powershell.exe -Command ...` / `pwsh.exe -EncodedCommand ...` if relevant to the harness.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Single-letter vars t/b 📜 Skill insight ⚙ Maintainability
Description
In scripts/security-audit.mjs, the new loops use non-descriptive single-letter variables (t,
b, n), reducing readability in security-gating logic and making future maintenance riskier.
Code

scripts/security-audit.mjs[R108-118]

+  if (tolerated.length) {
+    console.log('Tolerated advisories (vendored/bundled, unpatchable downstream — track upstream):');
+    for (const t of tolerated) console.log(`  - ${t.name} (${t.severity}) [${t.ids.join(', ')}]`);
+  }
+
+  if (blocking.length) {
+    console.error('\nBlocking high/critical advisories in our dependency tree:');
+    for (const b of blocking) {
+      console.error(`  - ${b.name} (${b.severity})`);
+      for (const n of b.nodes || []) console.error(`      ${n}`);
+    }
Relevance

●●● Strong

Readability nit with easy rename; team frequently accepts small maintainability cleanups in scripts.

PR-#439

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The checklist requires descriptive variable names; the newly added loops introduce t, b, and n
instead of intent-revealing names.

scripts/security-audit.mjs[108-118]
Skill: code-patterns

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Single-letter variable names (`t`, `b`, `n`) were introduced in new loops, which reduces clarity.

## Issue Context
These loops are part of security audit gating output; descriptive names help prevent mistakes during future changes.

## Fix Focus Areas
- scripts/security-audit.mjs[108-118]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Node engine mismatch risk 🐞 Bug ☼ Reliability
Description
The PR forces brace-expansion to 5.0.8 via overrides, but the lockfile records that version as
requiring Node 20 || >=22 while the repo and CI still claim/test Node 18 support; this will at
least produce engine warnings on Node 18 and will hard-fail for any contributors/CI that run with
engine-strict enabled. Since the lockfile also marks it as dev: true, this mostly impacts
development/CI installs rather than runtime consumers, but it still conflicts with the project’s
Node >=18 contract.
Code

package.json[R114-118]

  "overrides": {
    "esbuild": ">=0.28.1",
    "protobufjs": ">=7.6.4 <8",
-    "ws": ">=8.21.0"
+    "ws": ">=8.21.0",
+    "brace-expansion": "5.0.8"
Relevance

●● Moderate

Team has accepted Node-version consistency fixes in docs, but no precedent rejecting intentional
dev-only engine bumps.

PR-#171

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The override is introduced in package.json, while the resulting resolved dependency in
package-lock.json explicitly requires Node 20+; meanwhile CI still runs npm ci on Node 18 and
the package’s own engines field states Node >=18.

package.json[41-43]
package.json[114-119]
package-lock.json[3569-3580]
.github/workflows/ci.yml[11-29]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`package.json` now overrides `brace-expansion` to `5.0.8`, but `package-lock.json` shows that version declares `engines.node: "20 || >=22"` while the repo and CI still support/run Node 18. This creates install-time engine warnings on Node 18 and will fail in environments that enable `engine-strict`.

## Issue Context
- CI runs `npm ci` on Node 18/20/22.
- Root `package.json` claims `engines.node: >=18.0.0`.
- `brace-expansion@5.0.8` requires Node 20+ per lockfile.

## Fix Focus Areas
- package.json[114-118]
- package-lock.json[3569-3580]
- .github/workflows/ci.yml[11-29]

## Suggested fix options
Pick one consistent strategy:
1. **If Node 18 remains supported:** avoid forcing a Node-20-only `brace-expansion` (use a patched version compatible with Node 18, or adjust the dependency chain so the vulnerable version is eliminated without a Node 20-only bump).
2. **If Node 20+ is now the real support floor:** update `package.json` `engines.node` and drop Node 18 from CI matrix to match.
3. **If you accept warnings but want fewer surprises:** explicitly document that Node 18 installs will warn and may fail under `engine-strict`, and consider setting `engine-strict=false` in CI (if it could ever be toggled) to prevent accidental future breakage.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Non-verb-noun main/adv 📜 Skill insight ⚙ Maintainability
Description
New function names main() and adv() do not follow the required verb-noun naming pattern, making
intent less clear (especially in security-audit gating code and its tests).
Code

scripts/security-audit.mjs[R104-107]

+function main() {
+  const report = runAudit();
+  const { blocking, tolerated } = evaluateAdvisories(report.vulnerabilities || {});
+
Relevance

●● Moderate

No clear repo precedent on verb-noun function naming for scripts; could be seen as subjective.

PR-#439

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The compliance rule requires verb-noun function names. The PR introduces function main() and
function adv(...), which are not verb-noun names.

scripts/security-audit.mjs[104-107]
tests/security-audit.test.js[19-25]
Skill: code-patterns

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
New function names `main` and `adv` do not follow the verb-noun naming convention.

## Issue Context
The checklist requires function names beginning with an action verb (e.g., `run...`, `make...`, `build...`, `evaluate...`).

## Fix Focus Areas
- scripts/security-audit.mjs[104-107]
- tests/security-audit.test.js[19-25]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

5. Docs omit interpreter-exec ✓ Resolved 📜 Skill insight ⚙ Maintainability
Description
This PR adds a new destructive category interpreter-exec, but docs/HARNESS.md’s
DESTRUCTIVE_CATEGORIES list is not updated, leaving documentation stale for the destructive-action
classifier behavior.
Code

src/core/harness/destructive-actions.js[R34-38]

  DB_DESTROY: 'db-destroy',
  REMOTE_EXEC: 'remote-exec',
  PERM_DESTROY: 'perm-destroy',
+  INTERPRETER_EXEC: 'interpreter-exec',
});
Relevance

●●● Strong

Doc drift/consistency fixes are commonly accepted when behavior changes.

PR-#171

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
DESTRUCTIVE_CATEGORIES now includes INTERPRETER_EXEC: 'interpreter-exec', but the documentation
section listing categories does not mention interpreter-exec, so it no longer reflects current
behavior.

src/core/harness/destructive-actions.js[27-38]
docs/HARNESS.md[648-671]
Skill: code-review-pr

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The destructive-action documentation list is stale after adding the new `interpreter-exec` category.

## Issue Context
`docs/HARNESS.md` documents `DESTRUCTIVE_CATEGORIES`, but it currently does not include the newly introduced `interpreter-exec` category.

## Fix Focus Areas
- src/core/harness/destructive-actions.js[27-38]
- docs/HARNESS.md[648-671]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Regression tests added in-place 📜 Skill insight ⚙ Maintainability
Description
This PR adds regression coverage by modifying several existing test files instead of introducing new
dedicated regression test files. This violates the guideline to keep regression tests in separate
new files to avoid coupling and churn in baseline test suites.
Code

tests/guard-cli.test.js[R95-137]

+test('builder gates interpreter-mediated writes on the same audited approval as any other destructive category (#477)', async () => {
+  const { root } = seedProject();
+  // The exact probes from the audit that previously classified as ALLOW.
+  const cases = [
+    `node -e "require('fs').writeFileSync('.rstack/runs/x/tasks.json','[]')"`,
+    `python3 -c "open('.rstack/runs/x/tasks.json','w').write('[]')"`,
+    `node -e "require('fs').utimesSync('.rstack/runs/x/tasks.json.lock',new Date(),new Date())"`,
+  ];
+  for (const command of cases) {
+    const blocked = await runGuard(['--command', command, '--task', 't1', '--project', root]);
+    assert.equal(blocked.code, 2, `expected block without approval: ${command}`);
+    assert.equal(blocked.verdict.category, 'interpreter-exec');
+    assert.match(blocked.verdict.reason, /requires approval/);
+  }
+
+  const runId = 'run-interp-approved';
+  const { root: approvedRoot } = seedProject({ runId, approvals: [approvedRecord('destructive-action:t1', runId)] });
+  const allowed = await runGuard(['--command', cases[0], '--task', 't1', '--project', approvedRoot, '--run-id', runId]);
+  assert.equal(allowed.code, 0, 'the same per-task destructive-action approval unblocks interpreter-exec too');
+  assert.equal(allowed.verdict.category, 'interpreter-exec');
+});
+
+test('validator/reviewer/security contexts deny interpreter-mediated writes outright, no approval escape hatch (#477)', async () => {
+  const denied = await runGuard(['--context', 'validator', '--command', `node -e "require('fs').writeFileSync('src/app.js','evil')"`], {
+    env: { RSTACK_ALLOW_DESTRUCTIVE: '1' }, // must NOT bypass the sandbox
+  });
+  assert.equal(denied.code, 2);
+  assert.equal(denied.verdict.category, 'validator-sandbox');
+  assert.match(denied.verdict.reason, /interpreter/i);
+
+  const bareInterpreter = await runGuard(['--context', 'reviewer', '--command', 'python3']);
+  assert.equal(bareInterpreter.code, 2, 'a bare interpreter with no script argument reads code from stdin');
+});
+
+test('a hard-BLOCKED task cannot bypass the attempt-budget gate via an interpreter one-liner (#373 x #477)', async () => {
+  const runId = 'run-blocked-interp';
+  const { root } = seedProject({ runId });
+  writeFileSync(join(root, '.rstack', 'runs', runId, 'tasks.json'), JSON.stringify([{ id: 't1', status: 'BLOCKED' }]));
+  const { code, verdict } = await runGuard(['--command', `node -e "require('fs').writeFileSync('x','y')"`, '--task', 't1', '--project', root, '--run-id', runId]);
+  assert.equal(code, 2);
+  assert.equal(verdict.category, 'attempt-budget-exhausted');
+});
+
Relevance

●● Moderate

Repo often adds new test files for regressions, but no clear rule/enforcement against editing
existing tests.

PR-#399
PR-#167

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1400748 requires regression tests to be added as new test files and forbids
modifying existing test files for regression coverage. The diff shows new regression-labeled test
cases (e.g., (#477)) added directly into multiple pre-existing test files.

tests/guard-cli.test.js[95-137]
tests/harness-destructive-actions.test.js[93-165]
tests/harness-validator-sandbox.test.js[124-162]
Skill: qa-testing

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Regression test cases were added by editing existing test files, but the compliance guideline requires regression tests to be added as new test files (e.g., `*.regression-###.test.js`) without modifying existing test files.

## Issue Context
This PR adds multiple tests explicitly referencing `#477` / `#373`, which are regression-style protections, but they were appended into existing suites such as `tests/guard-cli.test.js`, `tests/harness-destructive-actions.test.js`, and `tests/harness-validator-sandbox.test.js`.

## Fix Focus Areas
- tests/guard-cli.test.js[95-137]
- tests/harness-destructive-actions.test.js[93-165]
- tests/harness-validator-sandbox.test.js[124-162]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. isInterpreterExecCommand missing JSDoc ✓ Resolved 📜 Skill insight ⚙ Maintainability
Description
The exported isInterpreterExecCommand(command) function has a JSDoc block but it is missing the
required @param and @returns tags (and @throws if applicable), violating the project’s
documentation requirements for public/exported APIs. This prevents tooling and maintainers from
relying on consistent, complete JSDoc metadata.
Code

src/core/harness/destructive-actions.js[R197-211]

+/**
+ * True when `command` invokes a general-purpose interpreter with inline or
+ * stdin-fed code evaluation. Exported so validator-sandbox.js (deny-outright
+ * context) can reuse the exact same detector rather than re-deriving it with
+ * its own regex (the #477 gap: the two modules previously classified commands
+ * independently and both missed this class).
+ */
+export function isInterpreterExecCommand(command) {
+  if (typeof command !== 'string' || !command.trim()) return false;
+  for (const segment of command.split(/\|\||&&|[|;&\n]/)) {
+    const found = interpreterVerbAndArgs(segment);
+    if (found && interpreterHasEvalCapability(found.verb, found.args)) return true;
+  }
+  return false;
+}
Relevance

● Weak

Closest precedent: repo rejected adding JSDoc/docstring coverage-driven tags on exported functions.

PR-#469

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1400405 (and the related checklist requirement) states that exported/public
functions must include complete JSDoc metadata, including @param, @returns, and @throws tags
where applicable. In the cited code range, export function isInterpreterExecCommand(command) is
preceded by a descriptive JSDoc-style comment, but that comment does not include any @param or
@returns entries, demonstrating non-compliance with the required documentation format.

src/core/harness/destructive-actions.js[197-211]
src/core/harness/destructive-actions.js[197-210]
Skill: code-patterns
Skill: documentation-writing

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`isInterpreterExecCommand` is exported but its JSDoc comment is missing required tags (`@param`, `@returns`, and `@throws` if it can throw), so it does not meet the compliance/documentation format expected for public/exported functions.

## Issue Context
Compliance (PR Compliance ID 1400405 / checklist) requires that any exported/public function with a JSDoc/TSDoc block include a brief description plus `@param` entries for parameters and an `@returns` entry (and `@throws` if applicable) to ensure consistent API documentation for tooling and maintainers.

## Fix Focus Areas
- src/core/harness/destructive-actions.js[197-211]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment thread src/core/harness/destructive-actions.js
Comment on lines +108 to +118
if (tolerated.length) {
console.log('Tolerated advisories (vendored/bundled, unpatchable downstream — track upstream):');
for (const t of tolerated) console.log(` - ${t.name} (${t.severity}) [${t.ids.join(', ')}]`);
}

if (blocking.length) {
console.error('\nBlocking high/critical advisories in our dependency tree:');
for (const b of blocking) {
console.error(` - ${b.name} (${b.severity})`);
for (const n of b.nodes || []) console.error(` ${n}`);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

2. Single-letter vars t/b 📜 Skill insight ⚙ Maintainability

In scripts/security-audit.mjs, the new loops use non-descriptive single-letter variables (t,
b, n), reducing readability in security-gating logic and making future maintenance riskier.
Agent Prompt
## Issue description
Single-letter variable names (`t`, `b`, `n`) were introduced in new loops, which reduces clarity.

## Issue Context
These loops are part of security audit gating output; descriptive names help prevent mistakes during future changes.

## Fix Focus Areas
- scripts/security-audit.mjs[108-118]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +104 to +107
function main() {
const report = runAudit();
const { blocking, tolerated } = evaluateAdvisories(report.vulnerabilities || {});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

3. Non-verb-noun main/adv 📜 Skill insight ⚙ Maintainability

New function names main() and adv() do not follow the required verb-noun naming pattern, making
intent less clear (especially in security-audit gating code and its tests).
Agent Prompt
## Issue description
New function names `main` and `adv` do not follow the verb-noun naming convention.

## Issue Context
The checklist requires function names beginning with an action verb (e.g., `run...`, `make...`, `build...`, `evaluate...`).

## Fix Focus Areas
- scripts/security-audit.mjs[104-107]
- tests/security-audit.test.js[19-25]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread tests/guard-cli.test.js
Comment on lines +95 to +137
test('builder gates interpreter-mediated writes on the same audited approval as any other destructive category (#477)', async () => {
const { root } = seedProject();
// The exact probes from the audit that previously classified as ALLOW.
const cases = [
`node -e "require('fs').writeFileSync('.rstack/runs/x/tasks.json','[]')"`,
`python3 -c "open('.rstack/runs/x/tasks.json','w').write('[]')"`,
`node -e "require('fs').utimesSync('.rstack/runs/x/tasks.json.lock',new Date(),new Date())"`,
];
for (const command of cases) {
const blocked = await runGuard(['--command', command, '--task', 't1', '--project', root]);
assert.equal(blocked.code, 2, `expected block without approval: ${command}`);
assert.equal(blocked.verdict.category, 'interpreter-exec');
assert.match(blocked.verdict.reason, /requires approval/);
}

const runId = 'run-interp-approved';
const { root: approvedRoot } = seedProject({ runId, approvals: [approvedRecord('destructive-action:t1', runId)] });
const allowed = await runGuard(['--command', cases[0], '--task', 't1', '--project', approvedRoot, '--run-id', runId]);
assert.equal(allowed.code, 0, 'the same per-task destructive-action approval unblocks interpreter-exec too');
assert.equal(allowed.verdict.category, 'interpreter-exec');
});

test('validator/reviewer/security contexts deny interpreter-mediated writes outright, no approval escape hatch (#477)', async () => {
const denied = await runGuard(['--context', 'validator', '--command', `node -e "require('fs').writeFileSync('src/app.js','evil')"`], {
env: { RSTACK_ALLOW_DESTRUCTIVE: '1' }, // must NOT bypass the sandbox
});
assert.equal(denied.code, 2);
assert.equal(denied.verdict.category, 'validator-sandbox');
assert.match(denied.verdict.reason, /interpreter/i);

const bareInterpreter = await runGuard(['--context', 'reviewer', '--command', 'python3']);
assert.equal(bareInterpreter.code, 2, 'a bare interpreter with no script argument reads code from stdin');
});

test('a hard-BLOCKED task cannot bypass the attempt-budget gate via an interpreter one-liner (#373 x #477)', async () => {
const runId = 'run-blocked-interp';
const { root } = seedProject({ runId });
writeFileSync(join(root, '.rstack', 'runs', runId, 'tasks.json'), JSON.stringify([{ id: 't1', status: 'BLOCKED' }]));
const { code, verdict } = await runGuard(['--command', `node -e "require('fs').writeFileSync('x','y')"`, '--task', 't1', '--project', root, '--run-id', runId]);
assert.equal(code, 2);
assert.equal(verdict.category, 'attempt-budget-exhausted');
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Informational

5. Regression tests added in-place 📜 Skill insight ⚙ Maintainability

This PR adds regression coverage by modifying several existing test files instead of introducing new
dedicated regression test files. This violates the guideline to keep regression tests in separate
new files to avoid coupling and churn in baseline test suites.
Agent Prompt
## Issue description
Regression test cases were added by editing existing test files, but the compliance guideline requires regression tests to be added as new test files (e.g., `*.regression-###.test.js`) without modifying existing test files.

## Issue Context
This PR adds multiple tests explicitly referencing `#477` / `#373`, which are regression-style protections, but they were appended into existing suites such as `tests/guard-cli.test.js`, `tests/harness-destructive-actions.test.js`, and `tests/harness-validator-sandbox.test.js`.

## Fix Focus Areas
- tests/guard-cli.test.js[95-137]
- tests/harness-destructive-actions.test.js[93-165]
- tests/harness-validator-sandbox.test.js[124-162]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/core/harness/destructive-actions.js
Comment thread package.json
Comment on lines 114 to +118
"overrides": {
"esbuild": ">=0.28.1",
"protobufjs": ">=7.6.4 <8",
"ws": ">=8.21.0"
"ws": ">=8.21.0",
"brace-expansion": "5.0.8"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

7. Node engine mismatch risk 🐞 Bug ☼ Reliability

The PR forces brace-expansion to 5.0.8 via overrides, but the lockfile records that version as
requiring Node 20 || >=22 while the repo and CI still claim/test Node 18 support; this will at
least produce engine warnings on Node 18 and will hard-fail for any contributors/CI that run with
engine-strict enabled. Since the lockfile also marks it as dev: true, this mostly impacts
development/CI installs rather than runtime consumers, but it still conflicts with the project’s
Node >=18 contract.
Agent Prompt
## Issue description
`package.json` now overrides `brace-expansion` to `5.0.8`, but `package-lock.json` shows that version declares `engines.node: "20 || >=22"` while the repo and CI still support/run Node 18. This creates install-time engine warnings on Node 18 and will fail in environments that enable `engine-strict`.

## Issue Context
- CI runs `npm ci` on Node 18/20/22.
- Root `package.json` claims `engines.node: >=18.0.0`.
- `brace-expansion@5.0.8` requires Node 20+ per lockfile.

## Fix Focus Areas
- package.json[114-118]
- package-lock.json[3569-3580]
- .github/workflows/ci.yml[11-29]

## Suggested fix options
Pick one consistent strategy:
1. **If Node 18 remains supported:** avoid forcing a Node-20-only `brace-expansion` (use a patched version compatible with Node 18, or adjust the dependency chain so the vulnerable version is eliminated without a Node 20-only bump).
2. **If Node 20+ is now the real support floor:** update `package.json` `engines.node` and drop Node 18 from CI matrix to match.
3. **If you accept warnings but want fewer surprises:** explicitly document that Node 18 installs will warn and may fail under `engine-strict`, and consider setting `engine-strict=false` in CI (if it could ever be toggled) to prevent accidental future breakage.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@strix-security strix-security 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.

Strix flagged a new security finding below. See the pinned summary comment for the full PR status.

Comment on lines +173 to +178
function interpreterVerbAndArgs(segment) {
const tokens = segment.trim().split(/\s+/).filter(Boolean).map(stripQuotes);
if (!tokens.length) return null;
const verb = tokens[0].toLowerCase().replace(/.*[/\\]/, ''); // basename of /usr/bin/node etc.
return INTERPRETER_VERBS.has(verb) ? { verb, args: tokens.slice(1) } : null;
}

@strix-security strix-security Bot Jul 30, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Wrapped interpreter invocations still bypass the interpreter-exec guard

Resolved — no longer flagged as of 84f8397.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 3

🧹 Nitpick comments (1)
tests/security-audit.test.js (1)

106-112: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Redundant dynamic re-import.

TOLERATED_BUNDLED_ADVISORIES could be added to the static import on line 15 instead of a separate await import(...) here, since the module is already imported at module load time.

♻️ Proposed simplification
-import { evaluateAdvisories, isFullyBundled, advisoryIds } from '../scripts/security-audit.mjs';
+import { evaluateAdvisories, isFullyBundled, advisoryIds, TOLERATED_BUNDLED_ADVISORIES } from '../scripts/security-audit.mjs';
-test('the real TOLERATED_BUNDLED_ADVISORIES allowlist matches the documented brace-expansion GHSAs', async () => {
-  const { TOLERATED_BUNDLED_ADVISORIES } = await import('../scripts/security-audit.mjs');
-  assert.deepEqual(
+test('the real TOLERATED_BUNDLED_ADVISORIES allowlist matches the documented brace-expansion GHSAs', () => {
+  assert.deepEqual(
     [...TOLERATED_BUNDLED_ADVISORIES.get('brace-expansion')].sort(),
     ['GHSA-3jxr-9vmj-r5cp', 'GHSA-mh99-v99m-4gvg'].sort(),
   );
 });
🤖 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 `@tests/security-audit.test.js` around lines 106 - 112, Update the test to
include TOLERATED_BUNDLED_ADVISORIES in the existing static import from
scripts/security-audit.mjs, then remove the redundant await import inside the
test while preserving the current allowlist assertion.
🤖 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 `@src/core/harness/destructive-actions.js`:
- Around line 185-211: Update interpreterHasEvalCapability so the no-argument
check runs before the special deno handling, causing bare deno to be classified
as stdin-fed code evaluation like other bare interpreters. Preserve deno’s
existing eval-only argument behavior for invocations that include arguments.
- Around line 155-177: Update interpreterVerbAndArgs to recognize
version-suffixed interpreter executable names, such as python3.11, ruby3.2, and
perl5.34, while preserving path basename normalization and existing exact
matches. Normalize or match the extracted verb against the supported
INTERPRETER_VERBS entries before returning the interpreter metadata, and keep
unsupported executables returning null.
- Around line 141-211: Extend isInterpreterExecCommand and its helper parsing so
supported shell wrappers and launchers cannot hide interpreter eval execution.
Recognize bash, sh, and zsh as inline/stdin-capable interpreters with -c
handling, and unwrap common prefixes such as env, nohup, xargs, and leading
VAR=value assignments before evaluating the nested command. Preserve existing
script-path behavior while ensuring wrapped node/python and equivalent
interpreter invocations are detected for both builder and validator callers.

---

Nitpick comments:
In `@tests/security-audit.test.js`:
- Around line 106-112: Update the test to include TOLERATED_BUNDLED_ADVISORIES
in the existing static import from scripts/security-audit.mjs, then remove the
redundant await import inside the test while preserving the current allowlist
assertion.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 57b3ee5e-a561-4cb8-8925-967a123bb52c

📥 Commits

Reviewing files that changed from the base of the PR and between cce8808 and fb4f997.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (11)
  • package.json
  • scripts/security-audit.mjs
  • src/commands/guard.js
  • src/core/harness/destructive-actions.js
  • src/core/harness/validator-sandbox.js
  • src/integrations/pi/rstack-sdlc.ts
  • tests/guard-cli.test.js
  • tests/harness-destructive-actions.test.js
  • tests/harness-validator-sandbox-hook.test.js
  • tests/harness-validator-sandbox.test.js
  • tests/security-audit.test.js

Comment thread src/core/harness/destructive-actions.js
Comment thread src/core/harness/destructive-actions.js Outdated
Comment thread src/core/harness/destructive-actions.js
…sses

Three independent reviewers (CodeRabbit, Strix, qodo) confirmed the same
class of bypass in isInterpreterExecCommand(): it only inspected the
FIRST whitespace token of a segment, so wrapping the interpreter behind a
launcher, a nested shell, or a version-suffixed binary evaded detection
entirely — reproduced live against the extension by CodeRabbit
(`bash -c "node -e ..."`) and Strix.

Concretely closes:
- Leading env-var assignments: `FOO=1 node -e ...`
- Launcher/wrapper commands: `env`/`sudo`/`timeout`/`nohup`/`command`/
  `nice`/`ionice`/`setsid`/`exec` node/python/etc, including stacked
  wrappers (`sudo env timeout 5 node -e ...`), each consuming its own
  flags/args before the real verb is found (bounded to 6 hops).
- Nested shells: `bash`/`sh`/`zsh`/`dash`/`ksh` are now themselves
  INTERPRETER_VERBS with `-c` as their eval flag — a shell is a
  general-purpose interpreter too, so `bash -c "node -e ..."` is caught
  on the OUTER bash invocation regardless of what's nested inside.
- Version-suffixed binaries: `python3.11`, `ruby3.2`, `perl5.34` (pyenv/
  asdf/system-package-manager convention) now normalize to their base
  interpreter name.
- Glued short flags: `-e'code'`, `-c"code"` (no space/`=` before the
  quoted value) are now recognized, scoped to <=2-char dash flags so a
  long `--flag` can't over-match as a prefix.
- Bare `deno` (no args): previously fell through to `args[0] undefined
  !== 'eval'` and returned false, unlike every other bare interpreter in
  this file (which correctly treat "no args at all" as stdin-fed code) —
  now consistent.

The validator sandbox reuses this exact detector, so every fix here
closes the same gap in both the builder approval-gate and the
deny-outright validator/reviewer/security path with one change.

Also: added @param/@returns to the exported isInterpreterExecCommand
JSDoc (qodo), and filled in docs/HARNESS.md's DESTRUCTIVE_CATEGORIES
list — it was missing interpreter-exec (this PR's own new category) plus
two pre-existing gaps (remote-exec, perm-destroy) found while fixing it.

Adversarial test coverage added for every bypass form above (wrapper
chains, nested shells, version suffixes, glued flags, bare deno) in both
the classifier and validator-sandbox suites, plus a false-positive check
confirming the fixes don't flag ordinary sudo/env/timeout/nohup usage.
1644/1644 tests, typecheck clean, lint clean.

Skipped as out of scope for this PR (all originate from the already-open
#473 branch merged in for CI, not from #477's own diff): security-audit.mjs
variable-naming/console.log/verb-noun-naming findings, and the Node-18-vs-
brace-expansion-5.0.8 engine note — belong to #473's review cycle.
Skipped as not applicable: "regression tests should be new files" (does
not match this codebase's actual, extensive convention of adding cases to
existing suite files) and `any`-typing in the claim gate (matches this
3000+-line file's pre-existing, pervasive typing convention; a scoped
retype was out of proportion to this fix).

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

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

🧹 Nitpick comments (1)
docs/HARNESS.md (1)

675-680: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Doc list omits dash/ksh, which the implementation now also treats as interpreters.

Minor completeness nit — the code's INTERPRETER_EVAL_FLAGS includes dash and ksh alongside bash/sh/zsh, but the doc's parenthetical list doesn't mention them.

🤖 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 `@docs/HARNESS.md` around lines 675 - 680, Update the interpreter list in the
`interpreter-exec` documentation entry to include `dash` and `ksh` alongside
`bash`/`sh`/`zsh`, matching the implementations covered by
`INTERPRETER_EVAL_FLAGS`; leave the surrounding inline/stdin and on-disk script
behavior unchanged.
🤖 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 `@src/core/harness/destructive-actions.js`:
- Around line 221-227: Update normalizeInterpreterVerb to include php in the
version-suffix normalization pattern, so binaries such as php7.4, php8.1,
php8.2, and php8.3 resolve to the base php verb while preserving existing
Python, Ruby, and Perl behavior.
- Around line 194-219: Update skipEnvAssignmentsAndWrappers so wrapper options
that consume a separate following value are skipped together with that value,
covering common sudo, nice, and ionice flags such as -u and -n. Preserve
existing assignment, flag, timeout-duration, hop-limit, and real-command
detection behavior so the walk reaches the interpreter verb and continues
failing closed through the shared classifier.

---

Nitpick comments:
In `@docs/HARNESS.md`:
- Around line 675-680: Update the interpreter list in the `interpreter-exec`
documentation entry to include `dash` and `ksh` alongside `bash`/`sh`/`zsh`,
matching the implementations covered by `INTERPRETER_EVAL_FLAGS`; leave the
surrounding inline/stdin and on-disk script behavior unchanged.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8d40525e-37ca-4e3a-9fbd-d7dea19b6b75

📥 Commits

Reviewing files that changed from the base of the PR and between fb4f997 and f6a78f8.

📒 Files selected for processing (4)
  • docs/HARNESS.md
  • src/core/harness/destructive-actions.js
  • tests/harness-destructive-actions.test.js
  • tests/harness-validator-sandbox.test.js

Comment thread src/core/harness/destructive-actions.js
Comment thread src/core/harness/destructive-actions.js
@richard-devbot

Copy link
Copy Markdown
Owner Author

@strix-security please re-run — the interpreter-detector bypass (wrappers, nested shells, version-suffixed binaries) reported in the previous review has been fixed.

…rsion suffix

A second Strix pass (after re-requesting review post-fix) confirmed the
first round of wrapper/version-suffix fixes but found two residual gaps:

1. Wrapper flags that take their value as a SEPARATE following token
   (nice -n 10, ionice -c 3, sudo -u root, timeout --signal KILL 5) — the
   value token (`10`, `root`, `KILL`) was mistaken for the next command's
   verb and returned null, since the previous fix only knew how to skip
   tokens that themselves look like flags/assignments, not "a flag plus
   its separate value". Fixed with a per-wrapper WRAPPER_VALUE_FLAGS map
   naming which flags consume a following token, so `nice -n 10 node -e
   ...` now correctly walks past `-n 10` to find `node`.

2. Version-suffixed `php` binaries (`php7.4`, `php8.2` — distro package
   naming) weren't in the version-suffix normalization regex, which only
   covered python/ruby/perl. Added.

Adversarial test coverage for both: every wrapper-with-value-flag
combination Strix's report implied, plus a false-positive check that
ordinary `nice`/`sudo`/`ionice` usage on non-interpreter commands stays
unflagged. 1646/1646 tests, typecheck clean, lint clean.

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

Copy link
Copy Markdown
Owner Author

@strix-security please re-run — the two follow-up gaps (wrapper flags with separate-token values, php version-suffix) have been fixed.

@strix-security strix-security 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.

Strix flagged a new security finding below. See the pinned summary comment for the full PR status.

Comment on lines +277 to +287
const lowerFlags = flagSet.map((f) => f.toLowerCase());
return args.some((raw) => {
const lower = stripQuotes(raw).toLowerCase();
if (lowerFlags.includes(lower)) return true;
if (lowerFlags.includes(lower.split('=')[0])) return true; // --flag=value
// A short single-dash flag glued directly to its value with no
// separating space or `=` (`-e'code'`, `-c"code"`). Scoped to <=2-char
// dash flags only, so this can't over-match a long --flag as a prefix
// of some unrelated longer token.
return lowerFlags.some((flag) => flag.length <= 2 && flag[0] === '-' && flag[1] !== '-' && lower.startsWith(flag) && lower.length > flag.length);
});

@strix-security strix-security Bot Jul 30, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Clustered shell short options still bypass the interpreter-exec guard

Resolved — no longer flagged as of 4e1cd88.

Third Strix pass confirmed both prior follow-up fixes and found one more
gap: clustered single-dash short options — `bash -lc "node -e ..."`
(login + command), `python3 -uc "..."` (unbuffered + command),
`perl -pie '...'` (print-loop + in-place + eval) — bundle an eval-capable
flag letter behind other single-char flags in the SAME token, which
neither the exact-match nor the glued-prefix check recognized (`-lc`
doesn't start with `-c`).

Fixed: interpreterHasEvalCapability now also checks a bounded clustering
pattern — a single-dash token of 1-4 lowercase letters where any letter
matches a known single-CHARACTER eval flag (`-c`/`-e`/`-r`/`-p`; long
`--flag` forms can't cluster, so they're excluded). The length bound
keeps this a plausible short-flag cluster rather than a long GNU-style
word that happens to contain the letter.

Adversarial coverage for the exact reported forms plus their real,
common non-eval clusters (`python3 -O`, `ruby -w`, `bash -lx`) staying
unflagged. 1648/1648 tests, typecheck clean, lint clean.

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

Copy link
Copy Markdown
Owner Author

@strix-security please re-run — the clustered-short-option gap (bash -lc, python -uc, perl -pie) has been fixed.

@strix-security strix-security 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.

Strix flagged 2 new security findings below. See the pinned summary comment for the full PR status.

Comment thread src/core/harness/destructive-actions.js Outdated
// `10` in `-n 10` was mistaken for the next command's verb and the real
// interpreter one hop further in was never reached.
const WRAPPER_VALUE_FLAGS = Object.freeze({
env: new Set(['-u', '--unset', '-c', '--chdir', '-s', '--split-string']),

@strix-security strix-security Bot Jul 30, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Interpreter-exec guard still misses env -C and longer clustered short-option forms

Resolved — no longer flagged as of 9b13559.

Comment thread src/core/harness/destructive-actions.js Outdated
Comment on lines +295 to +298
if (/^-[a-z]{1,4}$/.test(lower)) {
const clusteredLetters = lower.slice(1);
if (lowerFlags.some((flag) => flag.length === 2 && flag[0] === '-' && clusteredLetters.includes(flag[1]))) return true;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Inspect longer clustered short-option tokens for embedded eval flags

Suggested change
if (/^-[a-z]{1,4}$/.test(lower)) {
const clusteredLetters = lower.slice(1);
if (lowerFlags.some((flag) => flag.length === 2 && flag[0] === '-' && clusteredLetters.includes(flag[1]))) return true;
}
if (/^-[a-z]{2,}$/.test(lower)) {
const clusteredLetters = lower.slice(1);
if (lowerFlags.some((flag) => flag.length === 2 && flag[0] === '-' && clusteredLetters.includes(flag[1]))) return true;
}

…unded cluster length (Strix 3rd follow-up)

Two residual bypasses on the interpreter-exec detector: (1) env's real
chdir flag is uppercase `-C` (distinct from `-c`), but the wrapper
value-flags lookup compared tokens case-sensitively against a
lowercase-only set, so `env -C /dir node -e ...` never consumed the
directory argument and mistook it for the next command's verb,
stopping short of the wrapped interpreter; matching is now
case-insensitive. (2) the clustered-short-option regex was bounded to
4 letters, missing longer real clusters like `bash -lifc "..."`;
widened to no upper bound (2+ letters) per Strix's suggested diff.
@richard-devbot

Copy link
Copy Markdown
Owner Author

@strix-security please re-run — fixed the env -C (uppercase chdir flag, case-insensitive matching now) and widened the clustered short-option regex to no upper bound per your suggested diff.

@strix-security strix-security 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.

Strix flagged a new security finding below. See the pinned summary comment for the full PR status.

Comment on lines +205 to +241
const WRAPPER_VALUE_FLAGS = Object.freeze({
env: new Set(['-u', '--unset', '-c', '--chdir', '-s', '--split-string']),
sudo: new Set(['-u', '--user', '-g', '--group', '-p', '--prompt', '-h', '--host', '-r', '--role', '-t', '--type']),
nice: new Set(['-n', '--adjustment']),
ionice: new Set(['-c', '--class', '-n', '--classdata', '-p', '--pid', '-t', '--ignore']),
timeout: new Set(['-s', '--signal', '-k', '--kill-after']),
});

// Index into `tokens` of the real command verb, after skipping any leading
// `VAR=value` environment assignments and a bounded chain of launcher
// wrappers (each consuming its own flags/args, INCLUDING a separate value
// token for flags known to take one). Returns `tokens.length` when nothing
// but assignments/wrappers are present (no real command at all).
function skipEnvAssignmentsAndWrappers(tokens) {
let i = 0;
while (i < tokens.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[i])) i += 1;
for (let hop = 0; hop < MAX_WRAPPER_HOPS && i < tokens.length; hop += 1) {
const verb = tokens[i].toLowerCase().replace(/.*[/\\]/, '');
if (!WRAPPER_VERBS.has(verb)) break;
i += 1;
const valueFlags = WRAPPER_VALUE_FLAGS[verb];
while (i < tokens.length) {
const token = tokens[i];
if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(token)) { i += 1; continue; }
// Bare numeric duration for `timeout` (`timeout 5 node -e ...`).
if (verb === 'timeout' && /^[0-9]+(\.[0-9]+)?[smhd]?$/i.test(token)) { i += 1; continue; }
if (token.startsWith('-')) {
i += 1;
// A flag known to take a separate value token consumes the next
// token too, unless the value was already glued via `=`. Matched
// case-insensitively: GNU env's real chdir flag is `-C` (uppercase,
// distinct from `-c`) — an exact-case Set lookup silently missed it
// (Strix follow-up, 3rd pass), leaving `-C`'s directory argument
// mistaken for the next command's verb and the wrapped interpreter
// one hop further in never reached.
if (valueFlags?.has(token.toLowerCase()) && !token.includes('=') && i < tokens.length) i += 1;
continue;

@strix-security strix-security Bot Jul 30, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

env -S and env --split-string still bypass the interpreter-exec guard

Resolved — no longer flagged as of a1cec3d.

…mmand instead of skipping it (Strix 4th follow-up)

GNU env's -S/--split-string re-parses its argument into the actual
command line, unlike other value-taking wrapper flags whose value is
opaque. Treating it like -u/-C and skipping the next token discarded
the wrapped interpreter itself (`env -S 'node -e "1"'` went
undetected). Naive whitespace tokenization already breaks the quoted
payload into the same tokens the wrapped command would produce, so
the fix consumes nothing extra after -S/--split-string and lets the
walk continue straight into them.
@richard-devbot

Copy link
Copy Markdown
Owner Author

@strix-security please re-run — fixed env -S/--split-string: it re-parses its argument into the actual command line rather than taking an opaque value, so it no longer consumes an extra token (which was discarding the wrapped interpreter).

@strix-security strix-security 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.

Strix flagged a new security finding below. See the pinned summary comment for the full PR status.

Comment thread src/core/harness/destructive-actions.js Outdated
Comment on lines +239 to +242
if (token.startsWith('-')) {
const lowerToken = token.toLowerCase();
i += 1;
if (verb === 'env' && ENV_SPLIT_STRING_FLAGS.has(lowerToken)) continue;

@strix-security strix-security Bot Jul 30, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Glued env -S<cmd> still bypasses the interpreter-exec guard

Resolved — no longer flagged as of 775758a.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

🤖 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 `@src/core/harness/destructive-actions.js`:
- Around line 205-251: Update skipEnvAssignmentsAndWrappers to recognize GNU env
split-string options when the value is glued to -S or --split-string, including
quoted payloads and equals syntax. Extract and continue walking the embedded
command tokens rather than treating the entire option as an opaque flag, so
interpreter executions are detected by isInterpreterExecCommand and shared
classifiers fail closed as intended. Preserve existing handling for
separate-token split-string flags and other wrapper options.

In `@tests/harness-destructive-actions.test.js`:
- Around line 264-279: The env split-string detection must support glued -S and
--split-string= forms in addition to standalone tokens. Update the split-string
handling in isInterpreterExecCommand to recognize both glued variants while
preserving existing behavior, then add regression assertions for env -S'node -e
"1"' and env --split-string='python3 -c "1"' alongside the current tests.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e6296c58-e683-4ca8-bc95-c4578165d35f

📥 Commits

Reviewing files that changed from the base of the PR and between f6a78f8 and a1cec3d.

📒 Files selected for processing (2)
  • src/core/harness/destructive-actions.js
  • tests/harness-destructive-actions.test.js

Comment on lines +205 to +251
const WRAPPER_VALUE_FLAGS = Object.freeze({
env: new Set(['-u', '--unset', '-c', '--chdir']),
sudo: new Set(['-u', '--user', '-g', '--group', '-p', '--prompt', '-h', '--host', '-r', '--role', '-t', '--type']),
nice: new Set(['-n', '--adjustment']),
ionice: new Set(['-c', '--class', '-n', '--classdata', '-p', '--pid', '-t', '--ignore']),
timeout: new Set(['-s', '--signal', '-k', '--kill-after']),
});
// GNU env's `-S`/`--split-string` re-parses its argument into the underlying
// command line — it is NOT an ordinary value-taking flag whose value can be
// skipped (Strix follow-up, 4th pass): `env -S 'node -e "1"'` still runs
// node. Naive whitespace tokenization already breaks the quoted payload into
// the SAME subsequent tokens the wrapped command would produce, so the fix
// is to consume nothing extra here and let the walk continue straight into
// them, rather than discarding the next token as if it were a plain value.
const ENV_SPLIT_STRING_FLAGS = new Set(['-s', '--split-string']);

// Index into `tokens` of the real command verb, after skipping any leading
// `VAR=value` environment assignments and a bounded chain of launcher
// wrappers (each consuming its own flags/args, INCLUDING a separate value
// token for flags known to take one). Returns `tokens.length` when nothing
// but assignments/wrappers are present (no real command at all).
function skipEnvAssignmentsAndWrappers(tokens) {
let i = 0;
while (i < tokens.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[i])) i += 1;
for (let hop = 0; hop < MAX_WRAPPER_HOPS && i < tokens.length; hop += 1) {
const verb = tokens[i].toLowerCase().replace(/.*[/\\]/, '');
if (!WRAPPER_VERBS.has(verb)) break;
i += 1;
const valueFlags = WRAPPER_VALUE_FLAGS[verb];
while (i < tokens.length) {
const token = tokens[i];
if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(token)) { i += 1; continue; }
// Bare numeric duration for `timeout` (`timeout 5 node -e ...`).
if (verb === 'timeout' && /^[0-9]+(\.[0-9]+)?[smhd]?$/i.test(token)) { i += 1; continue; }
if (token.startsWith('-')) {
const lowerToken = token.toLowerCase();
i += 1;
if (verb === 'env' && ENV_SPLIT_STRING_FLAGS.has(lowerToken)) continue;
// A flag known to take a separate value token consumes the next
// token too, unless the value was already glued via `=`. Matched
// case-insensitively: GNU env's real chdir flag is `-C` (uppercase,
// distinct from `-c`) — an exact-case Set lookup silently missed it
// (Strix follow-up, 3rd pass), leaving `-C`'s directory argument
// mistaken for the next command's verb and the wrapped interpreter
// one hop further in never reached.
if (valueFlags?.has(lowerToken) && !token.includes('=') && i < tokens.length) i += 1;
continue;

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.

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

env -S/--split-string glued to its value still bypasses the interpreter-exec guard.

ENV_SPLIT_STRING_FLAGS.has(lowerToken) (line 242) only matches when -S/--split-string is its own, separate token — exactly the case the code comment (and past Strix follow-up) already fixed. But GNU env also accepts the value glued directly to the flag: -S'cmd' (no space) or --split-string=cmd. Confirmed via GNU coreutils docs: env -vS'perl -T -w' and "Running env -Sstring splits the string into arguments based on unquoted spaces."

For a glued token such as -S'node -e "1"', lowerToken is -s'node (not -s), so the split-string branch never triggers. The token instead falls into the generic token.startsWith('-') branch, which silently consumes the entire glued token as an opaque boolean flag — the embedded node -e "1" is never extracted or inspected. Reproduction:

env -S'node -e "1"'
env --split-string='node -e "1"'

Both execute node -e "1" under real GNU env, yet isInterpreterExecCommand(...) returns false for both, so classifyCommand, commandWritesFile, and the validator's deny-outright path (which reuses this detector) all miss it.

🔒 Proposed fix: detect glued `-S`/`--split-string` forms
 const ENV_SPLIT_STRING_FLAGS = new Set(['-s', '--split-string']);
+// Matches `-S` glued directly to its value (`-S'cmd'`, `-Scmd`) or the
+// long-option glued form (`--split-string=cmd`) — GNU env accepts both,
+// and neither is an exact match against ENV_SPLIT_STRING_FLAGS.
+const ENV_SPLIT_STRING_GLUED_RE = /^(?:-s.+|--split-string=.*)$/i;
 
 function skipEnvAssignmentsAndWrappers(tokens) {
   ...
       if (token.startsWith('-')) {
         const lowerToken = token.toLowerCase();
         i += 1;
-        if (verb === 'env' && ENV_SPLIT_STRING_FLAGS.has(lowerToken)) continue;
+        if (verb === 'env' && (ENV_SPLIT_STRING_FLAGS.has(lowerToken) || ENV_SPLIT_STRING_GLUED_RE.test(token))) continue;
         if (valueFlags?.has(lowerToken) && !token.includes('=') && i < tokens.length) i += 1;
         continue;
       }

As per coding guidelines, "Destructive actions, approvals, protected-config writes, and validator restrictions must fail closed and use the shared classifier."

🤖 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 `@src/core/harness/destructive-actions.js` around lines 205 - 251, Update
skipEnvAssignmentsAndWrappers to recognize GNU env split-string options when the
value is glued to -S or --split-string, including quoted payloads and equals
syntax. Extract and continue walking the embedded command tokens rather than
treating the entire option as an opaque flag, so interpreter executions are
detected by isInterpreterExecCommand and shared classifiers fail closed as
intended. Preserve existing handling for separate-token split-string flags and
other wrapper options.

Source: Coding guidelines

Comment on lines +264 to +279
// Strix follow-up, 4th pass: GNU env's `-S`/`--split-string` re-parses its
// argument into the underlying command line rather than taking an ordinary
// opaque value — treating it like `-u`/`-C` (skip one token) discarded the
// wrapped command itself instead of exposing it.
test('interpreter-exec: env -S / --split-string does not hide the wrapped interpreter (Strix 4th follow-up)', () => {
for (const cmd of [
`env -S 'node -e "1"'`,
`env --split-string 'python3 -c "print(1)"'`,
]) {
assert.equal(isInterpreterExecCommand(cmd), true, `expected interpreter-exec: ${cmd}`);
}
});

test('interpreter-exec: env -S fix does not create a false positive when nothing evals', () => {
assert.equal(isInterpreterExecCommand(`env -S 'npm test'`), false);
});

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Missing regression coverage for glued -S/--split-string forms.

Coverage here only exercises the space-separated form (env -S 'node -e "1"', env --split-string 'python3 -c "print(1)"'). It doesn't cover the glued forms (env -S'node -e "1"', env --split-string='python3 -c "1"'), which GNU env also accepts and which bypass the current detector (see companion comment on src/core/harness/destructive-actions.js lines 205-251).

  • src/core/harness/destructive-actions.js#L205-L251: extend the split-string check to also match glued -S/--split-string= forms, not just the exact standalone token.
  • tests/harness-destructive-actions.test.js#L264-L279: add cases for env -S'node -e "1"' and env --split-string='python3 -c "1"' alongside the existing space-separated assertions.
🤖 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 `@tests/harness-destructive-actions.test.js` around lines 264 - 279, The env
split-string detection must support glued -S and --split-string= forms in
addition to standalone tokens. Update the split-string handling in
isInterpreterExecCommand to recognize both glued variants while preserving
existing behavior, then add regression assertions for env -S'node -e "1"' and
env --split-string='python3 -c "1"' alongside the current tests.

…he wrapped command (Strix 5th follow-up)

The just-fixed env -S/--split-string handling only recognized the flag
as a standalone token. GNU env also accepts the glued long form
(--split-string=<cmd>), which left the wrapped command embedded inside
that one token instead of exposed as the next token to walk into.
Normalize the flag by splitting on `=` before matching, and when glued,
substitute the payload (quote-stripped) in place of the flag token so
the walk continues straight into the wrapped interpreter exactly like
the space-separated form.
@richard-devbot

Copy link
Copy Markdown
Owner Author

@strix-security please re-run — fixed the glued env --split-string= form: it's now normalized on '=' before matching, and the payload is substituted in place so the walk continues into the wrapped interpreter.

@richard-devbot

Copy link
Copy Markdown
Owner Author

@strix-security this finding appears stale — the glued --split-string=<cmd> fix is already in the current HEAD (775758a) and I've independently verified: isInterpreterExecCommand("env --split-string='node -e \"1\"'") returns true on this commit. Please re-run against the latest commit to confirm.

@strix-security strix-security 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.

Strix flagged 3 new security findings below. See the pinned summary comment for the full PR status.

// detector (arbitrarily deep obfuscation is a losing regex battle — the
// approval-gate default for anything unrecognized as this class stays
// "allowed", same as before this module existed).
const WRAPPER_VERBS = new Set(['env', 'sudo', 'command', 'nohup', 'timeout', 'nice', 'ionice', 'setsid', 'exec']);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 cmd /c wrapper bypasses the new interpreter-exec guard

Severity: HIGH · CWE-184

The new interpreter-exec classifier only unwraps a fixed set of launcher verbs and omits Windows cmd. As a result, commands such as cmd /c node -e "..." and cmd /c python3 -c "..." are treated as non-destructive even though they execute arbitrary inline code. Because validator-sandbox.js reuses the same detector, the bypass affects both builder approval gating and validator read-only enforcement.

Prompt to fix with AI
This is a security vulnerability found during a code review.

Vulnerability: `cmd /c` wrapper bypasses the new interpreter-exec guard
Severity: HIGH
CWE: CWE-184

The new interpreter-exec classifier only unwraps a fixed set of launcher verbs and omits Windows `cmd`. As a result, commands such as `cmd /c node -e "..."` and `cmd /c python3 -c "..."` are treated as non-destructive even though they execute arbitrary inline code. Because `validator-sandbox.js` reuses the same detector, the bypass affects both builder approval gating and validator read-only enforcement.

Location: src/core/harness/destructive-actions.js:194-194
Context: Wrapper allowlist omits Windows `cmd`, so `cmd /c ...` never unwraps to the nested interpreter
```
const WRAPPER_VERBS = new Set(['env', 'sudo', 'command', 'nohup', 'timeout', 'nice', 'ionice', 'setsid', 'exec']);
```

Location: src/core/harness/destructive-actions.js:284-290
Context: Interpreter classification depends entirely on the wrapper walker, so an unrecognized `cmd` wrapper yields a false negative
```
function interpreterVerbAndArgs(segment) {
  const tokens = segment.trim().split(/\s+/).filter(Boolean).map(stripQuotes);
  if (!tokens.length) return null;
  const verbIndex = skipEnvAssignmentsAndWrappers(tokens);
  if (verbIndex >= tokens.length) return null;
  const verb = normalizeInterpreterVerb(tokens[verbIndex]);
  return INTERPRETER_VERBS.has(verb) ? { verb, args: tokens.slice(verbIndex + 1) } : null;
}
```

Location: src/core/harness/validator-sandbox.js:74-87
Context: Validator enforcement inherits the same false negative because it reuses `isInterpreterExecCommand()` unchanged
```
  Object.freeze({
    // General-purpose interpreters (#477): `node -e "fs.writeFileSync(...)"`
    // or a bare `python3` fed code over stdin/pipe is a write/read/exec
    // primitive invisible to the shell-verb rules above — a validator is
    // read-only, so ANY inline-eval or bare-stdin interpreter invocation is
    // denied outright, same posture as every other rule in this list (no
    // approval escape hatch). Uses a `test` predicate instead of a `pattern`
    // regex because the decision needs tokenization, not just text matching;
    // reuses the EXACT same detector destructive-actions.js gates the
    // builder path with (#131: one source of truth for the classification
    // itself, even though builder/validator apply different POLICIES to it).
    id: 'interpreter-exec',
    test: isInterpreterExecCommand,
    reason: 'invokes a general-purpose interpreter with inline/stdin code evaluation (arbitrary read/write/exec capability)',
  }),
```

How to fix:
Teach the wrapper walker to treat Windows `cmd` as a launcher wrapper and consume its execution flags before resolving the underlying verb. In practice, the detector should unwrap `cmd /c ...` and `cmd /k ...` the same way it already unwraps `env`, `sudo`, and other launchers, then re-run interpreter classification on the nested command. Add regression coverage for both `cmd /c node -e ...` and `cmd /c python3 -c ...` across `classifyCommand()`, `commandWritesFile()`, and validator-sandbox evaluation.

Please fix this vulnerability. If you propose a fix, make it concise and minimal.

React 👍 / 👎 to tune Strix for this repo. A repo collaborator (or the PR author) can resolve this thread to dismiss the finding.

Comment on lines +284 to +290
function interpreterVerbAndArgs(segment) {
const tokens = segment.trim().split(/\s+/).filter(Boolean).map(stripQuotes);
if (!tokens.length) return null;
const verbIndex = skipEnvAssignmentsAndWrappers(tokens);
if (verbIndex >= tokens.length) return null;
const verb = normalizeInterpreterVerb(tokens[verbIndex]);
return INTERPRETER_VERBS.has(verb) ? { verb, args: tokens.slice(verbIndex + 1) } : null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Interpreter classification depends entirely on the wrapper walker, so an unrecognized cmd wrapper yields a false negative

Comment on lines +74 to +87
Object.freeze({
// General-purpose interpreters (#477): `node -e "fs.writeFileSync(...)"`
// or a bare `python3` fed code over stdin/pipe is a write/read/exec
// primitive invisible to the shell-verb rules above — a validator is
// read-only, so ANY inline-eval or bare-stdin interpreter invocation is
// denied outright, same posture as every other rule in this list (no
// approval escape hatch). Uses a `test` predicate instead of a `pattern`
// regex because the decision needs tokenization, not just text matching;
// reuses the EXACT same detector destructive-actions.js gates the
// builder path with (#131: one source of truth for the classification
// itself, even though builder/validator apply different POLICIES to it).
id: 'interpreter-exec',
test: isInterpreterExecCommand,
reason: 'invokes a general-purpose interpreter with inline/stdin code evaluation (arbitrary read/write/exec capability)',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Validator enforcement inherits the same false negative because it reuses isInterpreterExecCommand() unchanged

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.

[P0][Security] Block interpreter-mediated writes in builder guard and validator read-only sandbox

2 participants