Skip to content

fix(guardrails): close interpreter-producer write bypass under the PowerShell tool (#1199)#1200

Merged
kyle-sexton merged 13 commits into
mainfrom
fix/guardrails-ps-interpreter-write-bypass
Jul 24, 2026
Merged

fix(guardrails): close interpreter-producer write bypass under the PowerShell tool (#1199)#1200
kyle-sexton merged 13 commits into
mainfrom
fix/guardrails-ps-interpreter-write-bypass

Conversation

@kyle-sexton

Copy link
Copy Markdown
Contributor

Summary

Closes a live write-gate bypass. block-hook-bypass.sh classified only PowerShell cmdlet/redirect
write forms (ps::write_bypass) and then exit 0ed before the shell-agnostic scans — so an
interpreter-producer write, python3 -c "open('x','w').write('a')", executed unguarded through the
PowerShell tool while the identical command through Bash was blocked. Interpreter-producer writes are
shell-agnostic: whichever tool launches python3 -c, the write bypasses Write/Edit identically.

The interpreter rule now also runs on the PowerShell lane, mirroring the Bash lane's two-part shape:
the invocation is detected in a quote-blanked form (ps::blank_quoted_spans, so a quoted mention
stays inert) while the write indicators are scanned on the raw command, where they legitimately live
inside the quoted -c payload. PowerShell cmdlet/redirect coverage is unchanged. guardrails 0.14.2.

This was the gap the code already acknowledged in-comment as "deferred to A2b".

Note on provenance: this is one of the two real causes behind the reported "PowerShell guard bypass".
The other (disk-hygiene's engine gate being dropped when disk_hygiene_enabled is unconfigured) is
handled separately. An earlier hypothesis — that PreToolUse hooks don't fire for the PowerShell tool —
was falsified: they fire correctly, payload tool_name is literally PowerShell.

Test plan

Verified end-to-end through the actual hook (CC 2.1.218):

command tool before after
python3 -c "open('x','w').write('a')" PowerShell exit 0 (bypass) exit 2 (blocked ✓)
same Bash exit 2 exit 2 (unchanged ✓)
python3 -c "import pathlib; pathlib.Path('x').write_text('a')" PowerShell exit 0 exit 2 (blocked ✓)
python3 -c "import os; print(os.path.normpath('a/b'))" PowerShell exit 0 exit 0 (quiet ✓)
Write-Output 'run python3 -c open() later' PowerShell exit 0 exit 0 (quiet ✓)
Set-Content -Path f.txt -Value 'x' PowerShell exit 2 exit 2 (unchanged ✓)

Four regression fixtures added to the PowerShell section of block-hook-bypass.test.sh (two MUST-block,
two MUST-stay-quiet). shellcheck -S error clean.

Fresh-docs basis (CLAUDE.md mandate): tools-reference + hooks fetched this session; the PowerShell
tool's hook firing and payload tool_name were verified by live payload dump, not recalled.

Related

Closes #1199.
Sibling cause of the same report: #1195 (disk-hygiene engine gate dropped when the option is unconfigured).

…werShell tool

block-hook-bypass classified only PowerShell cmdlet/redirect write forms and then
exit 0'd before the shell-agnostic scans, so `python3 -c "open('x','w')…"` — the
identical command the Bash lane blocks — executed unguarded when issued through the
PowerShell tool. Reproduced end-to-end: same command, Bash blocked, PowerShell wrote
the file.

Run the interpreter rule on the PowerShell lane too, mirroring the Bash lane's
two-part shape: detect the INVOCATION in a quote-blanked form (ps::blank_quoted_spans,
so a quoted mention stays inert) and scan the RAW command for the write indicators,
which legitimately live inside the quoted -c payload. PowerShell cmdlet/redirect
coverage unchanged. Four regression fixtures added. Closes the in-comment
"deferred to A2b" gap. guardrails 0.14.2.

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

claude Bot commented Jul 23, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 2m 13s —— View job


Code Review

  • Gather context
  • Read changed files
  • Review fix logic in block-hook-bypass.sh
  • Review test cases in block-hook-bypass.test.sh
  • Review changelog and version bump
  • Post findings

Summary

This is a well-targeted, correctly-implemented fix for a real bypass. The root cause is clear and the solution mirrors the Bash lane's two-part detection shape appropriately for PowerShell context. Below are a few observations worth considering before merge.


Findings

1. Missing test: double-quoted mention in PowerShell

Severity: Low (observation, not a bug)

The four new fixtures cover the single-quote mention (Write-Output 'run python3 -c open() later'), but not the double-quote form (e.g. Write-Output "python3 -c open() later"). The mechanism handles it correctly — ps::blank_quoted_spans strips the double-quoted span, leaving Write-Output which doesn't match python3 -c. But unlike the Bash lane (which tests both "..." and '...' mention forms), the PS lane test matrix only tests one.

This is a completeness gap, not a functional bug. Worth adding to finish the matrix symmetrically.

2. _ps_exec_lc double-blanking interaction — confirmed correct, but worth annotating

block-hook-bypass.sh line 456–457

_ps_exec_lc="$(ps::blank_quoted_spans "$COMMAND")"
_ps_exec_lc="${_ps_exec_lc,,}"

ps::blank_quoted_spans applies two successive sed passes (single-quotes first, then double-quotes). For a command like python3 -c "import pathlib; pathlib.Path('x').write_text('a')":

  1. Single-quote pass strips 'x' and 'a'python3 -c "import pathlib; pathlib.Path().write_text()"
  2. Double-quote pass strips the whole string arg → python3 -c

This leaves python3 -c — which correctly matches the invocation regex. The blanking is on the right variable for the invocation check; $COMMAND_LC (the raw command) is correctly used for $_py_write. The logic is sound, but the sequence is non-obvious for a reader unfamiliar with ps::blank_quoted_spans. A one-liner comment linking to the function would help.

3. _py_write pattern on COMMAND_LC doesn't directly catch .write_text() / .write_bytes() — but catches them via pathlib

The _py_write regex is:

open[[:space:]]*\(|\.write[[:space:]]*\(|(^|[^[:alnum:]_])pathlib|(^|[^[:alnum:]_])path[[:space:]]*\(

\.write[[:space:]]*\( matches .write( but not .write_text( or .write_bytes( (the _text/_bytes suffix separates write from the (). However, pathlib.Path('x').write_text('a') is still caught by the (^|[^[:alnum:]_])pathlib branch ( pathlib in the import statement, space = [^[:alnum:]_]). This is correct and covered by test case 2.

This is a pre-existing characteristic of _py_write, not introduced by this PR, and the behavior is correct. Just noting it for completeness.

4. Variable _ps_exec_lc placed in global scope

block-hook-bypass.sh line 456

_ps_exec_lc is declared as a bare assignment inside the if [[ "$TOOL_NAME" == "PowerShell" ]] block (which exits after the check), so there's no scope leak. The _ prefix naming matches the existing pattern-variable convention in the file (_cat_redir, _py_write, etc.). No issue — just noting it's consistent with the file's style.


Changelog & Version Bump

Changelog entry is thorough and accurate — it describes the mechanism, the reproduction proof, and the test additions. Version bump from 0.14.10.14.2 is correct for a patch-level security fix.


Verdict

Approved with one optional follow-up: the double-quoted mention test is worth adding to complete the PS lane's test matrix (mirroring the Bash lane), but it is not blocking — the mechanism is correct and the primary attack paths are well-covered. The fix itself is correct and the implementation is sound.

@claude

claude Bot commented Jul 23, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 2m 54s —— View job


Security Review

  • Gather PR diff and changed files
  • Analyze block-hook-bypass.sh for logic flaws and injection surfaces
  • Analyze block-hook-bypass.test.sh for test integrity issues
  • Review plugin.json and CHANGELOG.md changes
  • Post security findings

No security vulnerabilities found in this PR's changes. Detail follows.

Changed files reviewed


Analysis

Injection surfaces — none introduced.

The new code at lines 456–460 passes $COMMAND into ps::blank_quoted_spans, which is implemented as printf '%s' "$text" | sed "s/'[^']*'//g". The sed expression is a literal string; user-controlled content from $COMMAND flows only through stdin to sed, never into the sed script itself. No command injection path.

Logic fidelity — correct two-part detection.

The invocation check runs on _ps_exec_lc (quote-blanked, lowercased), and the write-indicator check runs on the raw $COMMAND_LC. This mirrors the Bash lane's established pattern exactly (cf. lines 483–485). The $_py_write regex is reused unchanged from the Bash lane — no new regex surface. The unquoted $_py_write on the right-hand side of [[ =~ ]] is intentional Bash idiom for regex matching; this is consistent with every other use of _py_write in the file.

False-positive protection — holds.

For the quoted-mention test case Write-Output 'run python3 -c open() later': ps::blank_quoted_spans strips the single-quoted span via sed "s/'[^']*'//g", leaving only Write-Output — the invocation regex (^|[[:space:];|&()]+)python3[[:space:]]+-c does not match, so the if short-circuits regardless of what COMMAND_LC contains. The test fixture at line 498 covers this case.

No new attack surface on the exit 0 early-exit path.

The exit 0 at line 463 is the same early-exit that existed before this PR; the new check is inserted before it, so the original bypass (ps-only classify then exit before shell-agnostic scans) is closed. Order is: ps::write_bypass → new python3 check → emit_tel/exit 0.

Variable scoping — no collision risk.

_ps_exec_lc is defined and consumed entirely within the [[ "$TOOL_NAME" == "PowerShell" ]] block, which unconditionally terminates with exit 0. It cannot bleed into the Bash lane below.

Documented limitations (pre-existing, not introduced here).

ps::blank_quoted_spans is explicitly described as a "crude, SCAN-ONLY strip" (ps-command.sh:142). It does not handle Invoke-Expression/iex-mediated invocations, which are a separate attack surface addressed by other guards. Neither limitation is new to this PR — the function predates these changes and its constraints are carry-throughs, not regressions.

Test integrity.

The four new fixtures correctly cover the two must-block cases (open(), pathlib.write_text) and the two must-stay-quiet cases (read-only os.path.normpath, quoted mention). Test harness structure is unchanged. No fixtures that could be trivially subverted.


Verdict: no security findings. The fix is narrowly scoped, correctly mirrors the existing Bash lane pattern, introduces no new injection surfaces, and the false-positive guards are sound.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 86d06cc9dd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread plugins/guardrails/hooks/block-hook-bypass.sh Outdated
… lane (review)

Review P2: the PowerShell python-write indicator scan is raw-command-wide, so a
real python3 -c invocation compounded with an unrelated later segment mentioning an
indicator over-blocks. This is the identical accepted floor the Bash lane's
python-write rule already carries (fail-safe over-block, structurally unusual);
documented in-comment and kept byte-for-byte in step with the Bash lane rather than
diverging one tool's precision from the other.

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

claude Bot commented Jul 23, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 1m 25s —— View job


Review

  • Read changed files
  • Analyze new docs commit (f5b500e)
  • Evaluate Codex finding (P2: scope write indicators to -c payload)
  • Post review

Summary

This PR has two commits: the core fix (86d06cc) and a follow-up docs commit (f5b500e) that was added in direct response to the Codex P2 review finding. Both are well-executed. No blocking issues found.


f5b500e — Response to Codex P2 finding

The Codex review flagged (P2) that the _py_write scan runs against the full raw command rather than just the -c argument, which could over-block a compound like python3 -c "print(1)"; Write-Output "open(". The f5b500e commit responds by adding a detailed in-code comment at block-hook-bypass.sh:456–461 documenting the decision:

Accepted floor (identical to the Bash lane's python-write rule): the indicator scan is raw-command-wide, not scoped to the -c argument … Fail-safe direction (over-block, never under-block) and structurally unusual for LLM output; kept byte-for-byte in step with the Bash lane rather than diverging one tool's precision from the other.

This is the correct engineering call. Scoping to the -c argument would:

  1. Require parsing the shell-quoted payload reliably (fragile against escaped quotes and heredocs)
  2. Diverge the PS lane's precision from the Bash lane's for identical behavior, creating a maintenance bifurcation
  3. Move the failure mode from over-block to under-block for edge cases, which is the wrong direction for a security gate

The comment is accurate and the decision is well-reasoned. Codex P2 is resolved by documentation.


Remaining gap from prior code review

The earlier review noted as optional: adding a double-quoted mention test (Write-Output "python3 -c open() later") to symmetrically match the Bash lane's test matrix. The four new fixtures cover single-quote mention but not double-quote. This is still not addressed, and remains a low-severity completeness gap (not a bug — ps::blank_quoted_spans applies both single- and double-quote stripping, so the mechanism is correct).

This gap is optional-to-block, but worth tracking. Add test →


Fix logic — confirmed correct

The two-part detection shape at block-hook-bypass.sh:462–467 is a byte-for-byte structural mirror of the Bash lane at lines 489–492. The invocation check runs on the quote-blanked _ps_exec_lc; the write-indicator check runs on raw $COMMAND_LC. The new check is correctly inserted before emit_tel/exit 0, closing the bypass. Variable scoping, false-positive guards, and regex reuse are all sound (confirmed by the security review).


Verdict

Approved. The fix is correct, the Codex P2 concern is addressed by sound in-code documentation, and the security review found no vulnerabilities. The one remaining optional item (double-quoted mention test) is low-severity and non-blocking.

@claude

claude Bot commented Jul 23, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 4m 41s —— View job


Security Review

  • Gather context
  • Get PR diff
  • Read changed files
  • Analyze block-hook-bypass.sh logic for injection, bypass, and ordering issues
  • Post security findings

One security finding.


SUGGESTION — Backtick obfuscation bypasses the new python3 -c invocation check on the PowerShell lane

Confidence: HIGH — the bypass is mechanically confirmed and valid PowerShell syntax.

How ps::write_bypass handles backticks (unchanged file, ps-command.sh line 368–371):

scan=$(ps::blank_quoted_spans "$PS_BLANKED")
# Delete backticks before matching so a name obfuscated by PowerShell's escape
# char (`Set``-Content`) resolves to its real form.
scan="${scan//\`/}"

How the new python3 check handles the same input (block-hook-bypass.sh lines 462–467):

_ps_exec_lc="$(ps::blank_quoted_spans "$COMMAND")"
_ps_exec_lc="${_ps_exec_lc,,}"
# ← no backtick strip here
if [[ "$_ps_exec_lc" =~ (^|[[:space:];|&()]+)python3[[:space:]]+-c ]] && ...

Bypass scenario: python3 -c "open('x','w').write('a')"` through the PowerShell tool.

  1. ps::write_bypass sees no PowerShell write cmdlet or redirect → returns 1 (not blocked, flow continues).
  2. ps::blank_quoted_spans strips the double-quoted span → _ps_exec_lc = python3 `-c (lowercased).
  3. The invocation regex python3[[:space:]]+-c requires python3, whitespace, then immediately -c. The backtick between the space and - breaks the match → no match → exit 0 (bypass).
  4. In PowerShell, backtick before a character with no special meaning passes that character through unchanged: `-c-c. So PowerShell executes python3 -c "open('x','w').write('a')" — the write happens.

This is structurally parallel to the reason ps::write_bypass strips backticks at line 371: Set-Content `` obfuscation would otherwise evade its scanner. The new check is missing the same defence.

Fix: add one line after the lowercasing step to mirror ps::write_bypass:

_ps_exec_lc="$(ps::blank_quoted_spans "$COMMAND")"
_ps_exec_lc="${_ps_exec_lc,,}"
_ps_exec_lc="${_ps_exec_lc//\`/}"   # strip backticks — mirrors ps::write_bypass line 371

Fix this →


Remaining surface — no issues

Injection via ps::blank_quoted_spans: $COMMAND flows through printf '%s' "$text" | sed "s/'[^']*'//g" — user-controlled content only enters via stdin, never into the sed script itself. No injection path.

_py_write unquoted in [[ =~ ]]: intentional Bash idiom for regex variables; the value is a literal defined in the script (line 313), not from user input.

Execution ordering: the new block is inserted before emit_tel "ok" ""; exit 0, correctly closing the original bypass where exit 0 fired before the shell-agnostic scan.

Variable scoping: _ps_exec_lc is defined and consumed entirely inside the [[ "$TOOL_NAME" == "PowerShell" ]] block, which always terminates with exit 0. No bleed into the Bash lane.


@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f5b500e51a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread plugins/guardrails/hooks/block-hook-bypass.sh Outdated
…e invocation scan (review)

Review P2: ps::blank_quoted_spans removes ordinary quoted spans but not PowerShell
here-strings (@'...'@) or # comments, so inert text inside them mentioning both
python3 -c and a write indicator was read as an invocation and over-blocked (e.g.
a here-string body containing `python3 -c open(`). Chain ps::blank_herestrings +
blank_quoted_spans + a line-comment strip before the invocation regex — parity with
the Bash lane's strip_literals (which already drops heredocs and comments).
ps::blank_herestrings fail-safes to the raw command on an unbalanced here-string
(over-block, never under-block). Two regression fixtures added.

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

claude Bot commented Jul 24, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 2m 34s —— View job


Review

  • Gather context
  • Read changed files on latest commit (0353200)
  • Evaluate new commit against prior findings (backtick bypass, here-string/comment bypass, double-quoted mention test)
  • Post findings

What the latest commit (0353200) addresses

The Codex P2 finding — that ps::blank_quoted_spans leaves here-string bodies and # comments intact, so inert text like @'\npython3 -c open(\n'@ or # python3 -c open( was read as an invocation — is correctly fixed by the new commit.

The fix chains three operations before the invocation regex:

ps::blank_herestrings "$COMMAND"                               # blank @'...'@ and @"..."@ bodies → PS_BLANKED
_ps_exec_lc="$(ps::blank_quoted_spans "$PS_BLANKED")"          # blank ordinary 'x' and "x" spans
_ps_exec_lc="$(printf '%s' "$_ps_exec_lc" | sed 's/#.*$//')"  # strip # line-comments
_ps_exec_lc="${_ps_exec_lc,,}"

Mechanics verified:

  • ps::blank_herestrings (ps-command.sh:94) correctly drops here-string body lines and sets PS_BLANKED to the raw command on an unbalanced opener (fail-safe over-block).
  • sed 's/#.*$//' correctly strips #-comments per-line on the already-quote-blanked output, so # inside quoted spans is already gone before this runs.
  • Two new regression fixtures for here-string and line-comment cases are correctly written.

One remaining security gap — backtick bypass is still open

Confidence: HIGH — the bypass is mechanically reproducible.

The backtick bypass raised in the prior security review was NOT addressed by commit 0353200.

ps::write_bypass strips backticks at two points (ps-command.sh:353 and 371) with the explicit comment: "Delete backticks before matching so a name obfuscated by PowerShell's escape char resolves to its real form." The new python3 check is missing the equivalent step.

Bypass scenario: through the PowerShell tool:

python3 `-c "open('x','w').write('a')"

In PowerShell, ` before a non-special character passes through unchanged, so this executes identically to python3 -c "...". But in the guard:

  1. ps::blank_herestringsPS_BLANKED unchanged (no here-string)
  2. ps::blank_quoted_spans strips 'x', 'a', and the double-quoted span → python3 `-c
  3. Comment strip → unchanged
  4. Lowercase → python3 `-c
  5. Regex python3[[:space:]]+-c requires whitespace between python3 and -c. The backtick breaks the match → no match → exit 0 (bypass)

Fix: one line after lowercasing, mirroring ps::write_bypass:

_ps_exec_lc="${_ps_exec_lc,,}"
_ps_exec_lc="${_ps_exec_lc//\`/}"   # strip backticks — mirrors ps::write_bypass lines 353+371

Fix this →


Optional follow-ups (non-blocking)

Double-quoted mention testWrite-Output "python3 -c open() later" still not covered. The mechanism handles it correctly (ps::blank_quoted_spans blanks the double-quoted span), but the test matrix has the single-quote form and not the double-quote form. Raised as optional in the first code review; still open.

PowerShell block comments (<# ... #>) — the sed 's/#.*$//' strip does not handle <# multi-line #> block comments. This is a pre-existing gap not introduced by this PR, and the here-string fix covers the higher-risk multi-line inert-text vector. Worth noting for a future pass.


Verdict

The here-string/comment fix in 0353200 is correct and well-tested. The one remaining open item is the backtick bypass — a one-liner fix that mirrors ps::write_bypass's existing pattern. Given the backtick bypass was identified by the security review before the latest commit was pushed, it should be addressed before merge.

@claude

claude Bot commented Jul 24, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 4m 21s —— View job


Security Review

  • Get PR diff
  • Read changed files
  • Analyze block-hook-bypass.sh changes (including latest here-string/comment commit)
  • Check for backtick bypass (flagged in prior review)
  • Check for other injection surfaces and logic flaws
  • Post findings

Two security findings. No injection surfaces or authorization gaps introduced by this PR.


IMPORTANT — Backtick obfuscation bypass remains open (unfixed from prior review)

Confidence: HIGH

The prior security review on f5b500e identified that python3 \-c "open('x','w').write('a')"evades the invocation check because no backtick stripping is applied to_ps_exec_lcbefore the regex match. Commit0353200` addressed here-strings and line comments but did not add backtick stripping. The bypass remains mechanically valid in the current code.

Trace through block-hook-bypass.sh:470–476:

Input: python3 \-c "open('x','w').write('a')"`

  1. ps::write_bypass — no PS cmdlet/redirect → returns 1.
  2. ps::blank_herestrings — no here-string → PS_BLANKED = raw command.
  3. ps::blank_quoted_spans "$PS_BLANKED" — strips "open('x','w').write('a')"python3 `-c
  4. sed 's/#.*$//' — no # present → unchanged: python3 `-c
  5. Regex python3[[:space:]]+-c — requires whitespace immediately before -c. The backtick is between the space and -cno match → exit 0 (bypass).
  6. In PowerShell, backtick before -c has no special meaning → -c passes through unchanged → python3 -c "open('x','w').write('a')" executes → file write succeeds.

ps::write_bypass strips backticks at ps-command.sh:371 for the same reason (Set\`-Content` obfuscation). The new check is missing this same defence.

Fix: one line after the lowercasing step at line 473:

_ps_exec_lc="${_ps_exec_lc//\`/}"   # mirrors ps::write_bypass line 371

Fix this →


IMPORTANT — PowerShell <# ... #> block-comment bypass (introduced by 0353200)

Confidence: HIGH

Commit 0353200 adds sed 's/#.*$//' at line 472 to strip # line comment forms. This pattern anchors on the first # character on the line. PowerShell's <# ... #> block-comment syntax also contains a # at its opening token, so sed 's/#.*$//' strips from the # in <# to the end of the line — severing the -c argument from the invocation in _ps_exec_lc and breaking the regex match.

Trace for input python3 <# comment #> -c "open('x','w').write('a')":

  1. ps::write_bypass — no PS write cmdlet → returns 1.
  2. ps::blank_herestrings<# ... #> is a block comment, not a here-string; PS_BLANKED = raw command.
  3. ps::blank_quoted_spans "$PS_BLANKED" — strips single- then double-quoted spans → python3 <# comment #> -c (the write-arg double-quote is stripped; #> and -c survive).
  4. sed 's/#.*$//' — the # in <# is the first # on the line → strips # comment #> -c → result: python3 <
  5. Regex python3[[:space:]]+-cpython3 < does not match → exit 0 (bypass).
  6. In PowerShell, <# comment #> is whitespace between tokens → command is python3 -c "open('x','w').write('a')"file write succeeds.

This also applies to the multi-line block-comment form (single-line is more representative since ps::blank_herestrings does not process <# ... #>).

Note: <# #> block comment handling is not part of ps::blank_herestrings's scope (here-strings only), so there is no upstream blanking to rely on. The # anchor in sed 's/#.*$//' is the root cause.

Fix: strip PowerShell block-comment spans from $PS_BLANKED before the ps::blank_quoted_spans call, or replace the sed line-comment stripper with a pattern that only matches bare # (not # preceded by <), e.g.:

# Strip line comments (# not preceded by <, which would be a block-comment opener)
_ps_exec_lc="$(printf '%s' "$_ps_exec_lc" | sed 's/\(^\|[^<]\)#.*$/\1/')"

This preserves <# on the same line, so python3 <# comment #> -c becomes python3 <# comment #> -c after sed (the <# survives, and the regex still doesn't match python3[[:space:]]+-c because <# comment #> is in the way) — but at least it closes the evasion where the strip removes -c entirely. A more complete fix would also blank the <# ... #> span contents before the invocation scan.

Fix this →


No other findings

Injection via ps::blank_herestrings / ps::blank_quoted_spans: $COMMAND flows through printf '%s' "$text" | sed ... — user-controlled content enters only via stdin, never into the sed script itself. No injection path.

_py_write unquoted in [[ =~ ]]: intentional Bash idiom for regex variables; _py_write is a literal defined in the script (not from user input).

Execution ordering: the new block inserts before emit_tel "ok" ""; exit 0, correctly closing the original bypass.

Variable scoping: _ps_exec_lc is defined and consumed entirely within the [[ "$TOOL_NAME" == "PowerShell" ]] block, which terminates with exit 0. No bleed into the Bash lane.

plugin.json / CHANGELOG.md: version bump and documentation only; no security surface.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0353200975

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread plugins/guardrails/hooks/block-hook-bypass.sh Outdated
Comment thread plugins/guardrails/hooks/block-hook-bypass.sh Outdated
…on python3 write evasions (review P1)

Two P1 evasions of the interpreter-producer block: `& 'python3' -c "open(...)"`
(the call operator runs a QUOTED command word that quote-blanking erased) and a
backtick line-continuation splitting `python3` from `-c`. Both are real writes that
exited 0. Mirror ps::write_bypass: delete backticks first (continuation joins under
[[:space:]]+, escape-obfuscation resolves), then run two invocation scans — a
call/dot-source of a quoted `python3` on the quote-INTACT text, and the bare
invocation on the quote-blanked text. Inert here-string/comment/quoted-arg mentions
still stay quiet; real writes (bare, unquoted-call-op, both quoted-call forms,
continuation) all block. Three P1 fixtures added.

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

claude Bot commented Jul 24, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 5m 17s —— View job


Review

  • Gather context
  • Read changed files at latest commit (4cf6ea4)
  • Evaluate latest commit against open security findings (backtick bypass, call-operator bypass, <# #> block-comment bypass)
  • Check for any new issues
  • Post findings

What 4cf6ea4 Addresses

The latest commit refactors the PS python3 invocation scan into a three-step normalization + two-scan shape, mirroring ps::write_bypass line for line:

_q="\"'"
ps::blank_herestrings "$COMMAND"
_ps_nobt="${PS_BLANKED//\`/}"                             # ← strip backticks
_ps_nobt="$(printf '%s' "$_ps_nobt" | sed 's/#.*$//')"   # ← strip line comments
_ps_lcq="${_ps_nobt,,}"                                   # ← lowercase, quote-INTACT
_ps_bare="$(ps::blank_quoted_spans "$_ps_nobt")"          # ← quote-blanked
_ps_bare="${_ps_bare,,}"
if [[ "$COMMAND_LC" =~ $_py_write ]] &&
  { [[ "$_ps_bare" =~ (^|[[:space:];|&()]+)python3[[:space:]]+-c ]] ||
    [[ "$_ps_lcq" =~ (^|[[:space:]\;\{\}\(\|\&])[.\&][[:space:]]*[$_q]python3[$_q][[:space:]]+-c ]]; }; then

Backtick bypass — FIXED. _ps_nobt="${PS_BLANKED//\/}"strips backticks before either invocation scan runs. `` python3-c "open(...)" `` → python3 -c "open(...)" → matches scan 1. Mirrors `ps::write_bypass` line 353 exactly.

Backtick-continuation bypass — FIXED. The same backtick deletion joins python3 `↵-c into python3 ↵-c; [[:space:]]+ in the regex matches the newline. Correctly blocked.

Call-operator bypass — FIXED. Scan 2 on _ps_lcq (quote-INTACT) uses the pattern [.\&][[:space:]]*[$_q]python3[$_q][[:space:]]+-c, which correctly catches & 'python3' -c and & "python3" -c — the same shape ps::write_bypass uses at line 355 to catch quoted writer names. Running this scan on the quote-INTACT _ps_lcq (not _ps_bare) is the correct architectural choice: blanking would erase the 'python3' evidence.

Pattern consistency. The call-op regex structure (^|[[:space:]\;\{\}\(\|\&])[.\&][[:space:]]*[$_q]python3[$_q] is an exact structural copy of ps::write_bypass's call-op check (same preceding character class, same [$q] quoting approach, same _q="\"'" convention). The \;, \{, etc. inside [...] are redundant escapes of non-special characters — consistent with the pre-existing codebase style.

Three new test fixtures correct. & 'python3', & "python3", and backtick-continuation all verified as blocked. Inert cases (here-string, line-comment) remain quiet.


Remaining Gaps — All Pre-Existing

1. <# ... #> inline block-comment bypass

The prior security review flagged this as introduced by 0353200. That attribution is inaccurate — the bypass pre-existed 0353200. Before that commit, ps::blank_quoted_spans left the <# comment #> span intact between python3 and -c, breaking the [[:space:]]+ match. After 0353200, sed 's/#.*$//' truncates at <# instead, producing python3 < — same bypassed result, different truncation point. 4cf6ea4 does not change the comment-strip step.

Input: python3 <# comment #> -c "open('x','w').write('a')"

Trace through current code: ps::blank_herestrings leaves PS_BLANKED = raw → backtick delete → unchanged → sed 's/#.*$//' strips # comment #> -c "open(...) (anchors on the # inside <#) → _ps_nobt = python3 < → neither invocation scan matches → exit 0 (bypass).

In PowerShell, <# ... #> is whitespace between tokens, so this is a real write.

This is not introduced or worsened by this PR. A follow-on fix would need to either blank <# ... #> spans before the comment strip, or adjust sed to avoid anchoring on <#. Fix suggestion →

2. Double-quoted mention test (non-blocking)

Write-Output "python3 -c open() later" — still not covered. Mechanism is correct (ps::blank_quoted_spans blanks the double-quoted span before scan 1, and the call-op pattern on _ps_lcq doesn't match a bare mention). But the test matrix has single-quote form only. Flagged since first code review; still optional.


Verdict

The two open P1 security findings from prior reviews are resolved. The backtick obfuscation bypass, backtick line-continuation bypass, and call-operator bypass are all correctly fixed, and the implementation mirrors ps::write_bypass's established patterns structurally. No new security issues are introduced by this commit.

The <# ... #> inline block-comment bypass is a pre-existing gap, not introduced by this PR, and does not block merge — but it is the clearest remaining attack surface on the PS python3 lane and is worth tracking as a follow-on.

@claude

claude Bot commented Jul 24, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 4m 15s —— View job


Security Review

  • Gather context
  • Read PR diff and changed files
  • Analyze block-hook-bypass.sh for logic flaws, bypass vectors, and injection surfaces
  • Evaluate latest commit (4cf6ea4) against prior security findings (backtick bypass, block-comment bypass)
  • Check for any new injection surfaces or authorization gaps
  • Post security findings

Prior findings resolved by 4cf6ea4

The two P1 items from the prior security reviews are now fixed:

  • Backtick bypass (python3 \-c "...") — fixed. _ps_nobt="${PS_BLANKED//`/}"strips backticks before all downstream processing, mirroringps::write_bypass` line 353.
  • Backtick line-continuation (python3 \\n-c "...") — fixed by the same backtick strip; the newline collapses under [[:space:]]+` in the invocation regex.
  • Call-operator with quoted python3 (& 'python3' -c "...", & "python3" -c "...") — fixed. The second invocation scan (line 483) matches [.\&][[:space:]]*[$_q]python3[$_q][[:space:]]+-c on _ps_lcq (the quote-intact form), mirroring how ps::write_bypass detects a quoted writer name on lcq.

No new injection surfaces introduced. $COMMAND flows through printf '%s' | sed ... in every case — user-controlled content enters only via stdin, not the sed script. _py_write is a script literal used unquoted in [[ =~ ]] as intended Bash idiom.


IMPORTANT — PowerShell <# ... #> block-comment bypass remains open

Confidence: HIGH — mechanically confirmed.

Flagged in the prior security review (0353200 pass). Commit 4cf6ea4 addressed backtick and call-operator evasions but did not address block-comment evasion.

Mechanism. In PowerShell, <# ... #> is a block comment; it is treated as whitespace between tokens. So python3 <# comment #> -c "open('x','w').write('a')" executes identically to python3 -c "open('x','w').write('a')" and writes the file.

**Trace through block-hook-bypass.sh:475–484 for input python3 <# comment #> -c "open('x','w').write('a')":

  1. ps::blank_herestrings<# is not a here-string opener (@'/@"); PS_BLANKED = raw command unchanged.
  2. _ps_nobt="${PS_BLANKED//\/}"` — no backticks; unchanged.
  3. sed 's/#.*$//' — anchors on the first # on the line, which is the # in <# (column 9). Strips # comment #> -c "open('x','w').write('a')"_ps_nobt = python3 <.
  4. _ps_lcq="${_ps_nobt,,}" = python3 <.
  5. _ps_bare="$(ps::blank_quoted_spans "$_ps_nobt")" = python3 < (nothing to quote-blank).
  6. Invocation check 1: _ps_bare =~ (^|[[:space:];|&()]+)python3[[:space:]]+-cpython3 < does not match; < is not whitespace.
  7. Invocation check 2: _ps_lcq =~ ...[.\&]...[quote]python3[quote]...-cpython3 < has no call-operator prefix.
  8. Both checks fail → exit 0 (bypass). The real command writes the file unguarded.

The same bypass applies to the call-operator form: & <# comment #> 'python3' -c "..."sed strips from <# to EOL → & < → neither regex matches → exit 0.

Fix. Blank <# ... #> spans from PS_BLANKED before the normalization pipeline, or guard the comment-strip sed to not anchor on <#. The minimal one-liner is:

# Strip <# ... #> block-comment spans (single-line form) before the # line-comment strip
_ps_nobt="$(printf '%s' "${PS_BLANKED//\`/}" | sed 's/<#[^#]*#>//g')"
_ps_nobt="$(printf '%s' "$_ps_nobt" | sed 's/#.*$//')"

<#[^#]*#> covers the single-line inline form. Multi-line block comments (<# on one line, #> on another) are a harder case that may warrant a dedicated ps::blank_block_comments helper parallel to ps::blank_herestrings.

Fix this →


No other findings

All other surfaces are clean:

  • Variable scoping: _ps_nobt, _ps_lcq, _ps_bare, _q are all defined and consumed entirely within the [[ "$TOOL_NAME" == "PowerShell" ]] block, which terminates unconditionally with exit 0. No bleed into the Bash lane.
  • Execution ordering: the new block is correctly inserted before emit_tel "ok" ""; exit 0, closing the original bypass.
  • plugin.json / CHANGELOG.md: version bump and documentation only; no security surface.

Verdict

One remaining open item from the prior security review: the <# ... #> single-line block-comment bypass. The backtick and call-operator evasions that were the primary concern of the last two review rounds are correctly fixed by 4cf6ea4. The block-comment case is the last unresolved mechanical bypass on this lane; the fix is a one-liner sed pre-pass.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4cf6ea4f09

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread plugins/guardrails/hooks/block-hook-bypass.sh Outdated
…te-bypass lanes (review P1)

A path-qualified interpreter target bypassed the python-write gate on both
lanes: `/usr/bin/python3 -c "open(...)"` (Bash) and
`& 'C:\Python313\python3.exe' -c` / bare `C:\Python313\python3.exe -c`
(PowerShell). The command-word anchor required `python3` at a shell-separator
boundary with no path prefix and no extension, so the path separator before the
basename (and the `.exe` suffix) defeated the match, while `ps::write_bypass`
intentionally permits arbitrary quoted programs — a real file-write bypass for a
standard PowerShell call-operator form.

Both lanes now admit `/` `\` path separators in the command-word boundary and an
optional `.exe`; the PowerShell quoted-call scan additionally admits a path
prefix inside the quotes. All anchored on the `python3` BASENAME, so `notpython3`
and a non-python quoted exe (`& 'C:\...\app.exe'`) stay inert. Fixing both lanes
(not PowerShell only) preserves the documented Bash/PowerShell parity — the Bash
lane carried the identical hole.

Six regression fixtures added (abs-path + `.exe` Bash writes, quoted-path + bare
path PowerShell writes MUST block; `notpython3` and non-python quoted exe MUST
stay quiet). Suite PASS=185 FAIL=0. Folded into the unreleased 0.14.2 entry.

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

claude Bot commented Jul 24, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 5m 29s —— View job


Security Review

  • Gather PR diff
  • Read changed files
  • Analyze for security vulnerabilities
  • Post findings

One security finding. Every prior review round flagged it; a45dc75 (the final commit) did not close it.


IMPORTANT — PowerShell <# ... #> inline block-comment bypass

Confidence: HIGH — mechanically confirmed.

Root cause. The normalization pipeline at block-hook-bypass.sh:483 runs:

_ps_nobt="$(printf '%s' "$_ps_nobt" | sed 's/#.*$//')"

This strips from the first # on a line to end-of-line. PowerShell's <# ... #> inline block comment opens with <#, which contains # at column 2 of the token. sed 's/#.*$//' anchors on that # and strips everything from it through end-of-line — including the -c flag and the entire write payload that follow the block comment.

Bypass trace for python3 <# comment #> -c "open('x','w').write('a')" through the PowerShell tool:

  1. ps::blank_herestrings<# is not a here-string opener (@'/@"); PS_BLANKED = raw command.
  2. _ps_nobt="${PS_BLANKED//\/}"` — no backticks; unchanged.
  3. sed 's/#.*$//'# in <# is the first # on the line; strips # comment #> -c "open('x','w').write('a')"_ps_nobt = python3 <.
  4. _ps_lcq = python3 < — lowercased.
  5. _ps_bare = python3 <ps::blank_quoted_spans finds nothing to strip.
  6. Invocation scan 1 on _ps_bare: python3[[:space:]]+-cpython3 < has < after python3, not whitespace → no match.
  7. Invocation scan 2 on _ps_lcq: no call operator before python3no match.
  8. if branch is false → emit_tel "ok" ""; exit 0bypass.

In PowerShell, <# ... #> is whitespace between tokens, so this executes identically to python3 -c "open('x','w').write('a')" and the file write succeeds.

The same bypass applies to the call-operator form: & 'python3' <# comment #> -c "open('x','w').write('a')" — the sed strips -c and the payload from position <# onward, breaking scan 2's -c anchor.

Fix. Blank <# ... #> single-line spans from _ps_nobt before the line-comment sed runs. The inline form is a single-line problem; the multi-line form (<# and #> on separate lines) would require a ps::blank_block_comments helper parallel to ps::blank_herestrings, but the single-line case closes the most direct evasion:

_ps_nobt="${PS_BLANKED//\`/}"
# Blank inline <# ... #> block-comment spans before the # line-comment strip,
# or sed anchors on the # in <# and strips -c and the payload that follow.
_ps_nobt="$(printf '%s' "$_ps_nobt" | sed 's/<#[^#]*#>//g')"
_ps_nobt="$(printf '%s' "$_ps_nobt" | sed 's/#.*$//')"

Fix this →


a45dc75 — no new issues

The final commit's changes are clean:

  • PS lane scan 1 (_ps_bare regex) adds (\.exe)? and /\\ to the boundary class — identical to what the Bash lane now uses, correctly handles path-qualified bare invocations.
  • PS lane scan 2 (call-op regex) adds ([^$_q]*[\\/])? prefix inside the quoted group — correctly anchors on the python3 basename so & 'C:\Python313\python3.exe' -c is blocked while & 'C:\...\app.exe' with an incidental write indicator stays allowed.
  • Bash lane gets the same path-qualified regex update as the PS lane. No new attack surface.
  • $_q is a script literal ("'), not user-controlled; _ps_nobt flows only through stdin to sed, never into the sed script.

Previously resolved items (confirmed closed by 4cf6ea4)

  • ✅ Backtick obfuscation (python3 \-c "...") — ${PS_BLANKED//`/}` strip closes it.
  • ✅ Backtick line-continuation (python3 \↵-c "...") — same strip joins under [[:space:]]+`.
  • ✅ Call-operator with quoted python3 (& 'python3' -c "...") — scan 2 on _ps_lcq catches it.

The <# ... #> inline block-comment bypass is the last unresolved mechanical evasion on the PS python3 lane. Fix is a one-liner sed pre-pass.

@claude

claude Bot commented Jul 24, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 3m 35s —— View job


Review

  • Read changed files at latest commit (a45dc75)
  • Evaluate path-qualified python3 fix (P1 from last Codex review)
  • Re-check <# ... #> block-comment bypass (flagged as open in prior security reviews)
  • Check for any new issues introduced
  • Post findings

What a45dc75 Addresses

The Codex P1 finding — that a path-qualified interpreter target (/usr/bin/python3 -c, & 'C:\Python313\python3.exe' -c, bare C:\Python313\python3.exe -c) evaded both lanes because the command-word boundary didn't admit path separators or .exe — is correctly fixed by this commit.

Bash lane (line 517):

# Before
(^|[[:space:];|&()]+)python3[[:space:]]+-c
# After
(^|[[:space:];|&()/\\]+)python3(\.exe)?[[:space:]]+-c

/ and \ added to the boundary class; optional .exe suffix. Correctly handles /usr/bin/python3 -c and /c/Python313/python3.exe -c.

PS scan 1 (line 488) — same boundary change on _ps_bare (quote-blanked). For a bare path-qualified input like C:\Python313\python3.exe -c "...", the \ before python3 satisfies [[:space:];|&()/\\]+ → correctly blocked.

PS scan 2 (line 489):

# Before
[$_q]python3[$_q][[:space:]]+-c
# After
[$_q]([^$_q]*[\\/])?python3(\.exe)?[$_q][[:space:]]+-c

([^$_q]*[\\/])? allows an optional path prefix inside the quotes, anchored on the python3 basename. For & 'C:\Python313\python3.exe' -c: [^"']*[\\/] greedily takes c:\python313\, then python3 and .exe match, then the closing quote matches → correctly blocked.

Basename anchor verification. The notpython3 -c fixture correctly stays quiet: with no path separator before python3, and notpython3 not matching the boundary+python3 pattern. The & 'C:\...\app.exe' fixture stays quiet: apppython3 → no match (verified via regex backtracking analysis — the engine exhausts all ways to split app.exe after a [\\/] boundary, none of which produce python3).

[^$_q] in bash ERE: In [[ =~ ]], $_q expands to "' before the bracket expression is evaluated, so [^$_q] correctly becomes [^"']. Standard behavior, works as expected.

Six new test fixtures are well-chosen and correctly verify the new cases: two Bash must-block (abs-path python3, path-qualified python3.exe), one Bash must-quiet (notpython3), two PS must-block (quoted-path python3.exe, bare path python3.exe), one PS must-quiet (non-python quoted exe). Suite PASS=185 FAIL=0 per commit message.

CHANGELOG correctly updated — the 0.14.2 entry now documents path-qualified forms for both lanes.


One Remaining Gap — <# ... #> Block-Comment Bypass (Pre-Existing, Still Open)

Confidence: HIGH — previously confirmed by two independent security reviews; a45dc75 does not address it.

Mechanism. sed 's/#.*$//' at line 483 anchors on the first # character. PowerShell block-comment syntax <# ... #> also contains # at the opener, so the sed strip begins at <#, severing everything from that point to end-of-line — including -c when it follows the comment.

Trace for python3 <# comment #> -c "open('x','w').write('a')":

  1. ps::write_bypass — no PS cmdlet/redirect → returns 1.
  2. ps::blank_herestrings<# is not a here-string opener (@'/@") → PS_BLANKED = raw command.
  3. "${PS_BLANKED//\/}"— no backticks →_ps_nobt` = raw.
  4. sed 's/#.*$//' — anchors on # inside <# at column 9 → strips # comment #> -c "open('x','w').write('a')"_ps_nobt = python3 <.
  5. _ps_bare = ps::blank_quoted_spans("python3 <") = python3 < (nothing to blank).
  6. Scan 1: (^|[[:space:];|&()/\\]+)python3(\.exe)?[[:space:]]+-cpython3 < does not have [[:space:]]+-c following python3no match.
  7. Scan 2: requires [.\&] call-operator prefix → no match.
  8. Both scans fail → exit 0 (bypass). In PowerShell, <# comment #> is treated as whitespace between tokens → python3 -c "open('x','w').write('a')" executes → file written.

Fix (one-liner pre-pass before the # line-comment strip):

_ps_nobt="$(printf '%s' "$_ps_nobt" | sed 's/<#[^#]*#>//g')"   # blank inline <# ... #> spans
_ps_nobt="$(printf '%s' "$_ps_nobt" | sed 's/#.*$//')"          # strip # line-comments (existing)

<#[^#]*#> covers single-line inline block-comment spans. Multi-line <# / #> on separate lines is a harder case (not addressed here either, but substantially rarer as an LLM-generated evasion). A regression fixture: run_pwsh "PS: inline block-comment between python3 and -c (blocked)" "python3 <# comment #> -c \"open('x','w').write('a')\"" 2.

Fix this →


Optional Follow-Ups (Non-Blocking)

Double-quoted mention test (Write-Output "python3 -c open() later") — mechanism correct (ps::blank_quoted_spans blanks the double-quoted span before scan 1; _ps_lcq would only match if a call-operator preceded the quote), but the test matrix still only covers the single-quote form. Flagged since the first code review; still optional.


Verdict

The path-qualified python3 fix in a45dc75 is correct and complete — both lanes, both PS scan forms, six well-chosen fixtures, CHANGELOG updated. All prior P1 findings (backtick bypass, call-operator bypass, backtick-continuation, path-qualified target) are now resolved.

The one remaining open item is the <# ... #> inline block-comment bypass — a pre-existing gap first flagged in the 0353200 security review and still unaddressed. It is a real file-write bypass on the PS python3 lane, fixable with a one-liner sed pre-pass. Whether it blocks merge is a judgment call for the reviewer; the fix is small and the gap is the last mechanical bypass on this lane.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a45dc75a1d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread plugins/guardrails/hooks/block-hook-bypass.sh Outdated
…evasion (review P1)

A PowerShell invoked script block glues its first command onto the opening brace,
so `&{python3 -c "open('x','w').write('a')"}` (compact), `& {python3 …}` (spaced),
and `&{'python3' …}` (quoted name inside the block) executed the interpreter write
but exited 0: after quote-blanking the char before `python3` is `{`, absent from
the bare-invocation boundary class, and the quoted-call scan requires the quote to
hug the `&`.

Normalize `{`/`}` to spaces on the PowerShell lane before both derived scan forms,
so all three collapse to the already-covered `& python3` / `& 'python3'` shapes.
PowerShell-only: `&{cmd}` is a script-block invocation with no Bash `{ …; }`
analogue (that group needs surrounding spaces, already covered), so the Bash lane
needs no matching change — this is not a parity divergence.

Four regression fixtures added (compact / spaced / quoted-in-block writes MUST
block; a fully-quoted `&{python3 …}` string MUST stay quiet — brace normalization
does not defeat quote-blanking). Suite PASS=189 FAIL=0. Folded into 0.14.2.

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

claude Bot commented Jul 24, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 3m 58s —— View job


Review

  • Gather context and read changed files
  • Analyze latest commit (bd4a05f) — invoked-script-block fix
  • Check whether <# ... #> block-comment bypass is now addressed
  • Check for any new issues introduced
  • Post findings

What bd4a05f Addresses

The Codex P1 finding — that &{python3 -c "..."} (compact), & {python3 ...} (spaced), and &{'python3' -c "..."} (quoted name inside the block) all evaded the invocation scan because { is absent from the boundary classes — is correctly fixed.

Fix mechanism. Two lines added immediately after the comment strip and before both derived scan forms:

_ps_nobt="${_ps_nobt//\{/ }"
_ps_nobt="${_ps_nobt//\}/ }"

This normalizes {/} to spaces on _ps_nobt, which flows into both _ps_lcq (quote-intact) and _ps_bare (quote-blanked). All three invoked-script-block shapes then collapse to forms already covered:

  • &{python3 -c "open(...)}& python3 -c "open(...) " → scan 1 on _ps_bare matches [[:space:];|&()/\\]+python3[[:space:]]+-c
  • & {python3 -c "..."}& python3 -c "..." → same ✓
  • &{'python3' -c "..."}& 'python3' -c "..." → scan 2 on _ps_lcq matches [.\&][[:space:]]*[$_q]python3[$_q][[:space:]]+-c

False-positive guard holds. Write-Output '&{python3 -c open(}' (the must-stay-quiet fixture): brace normalization converts this to Write-Output '& python3 -c open( '. Then ps::blank_quoted_spans blanks the entire single-quoted span → _ps_bare = "write-output ". Scan 1 fails (no python3). Scan 2 on _ps_lcq: the & is at position 1 inside the single-quoted string (preceded by ', not by a call-operator boundary); the pattern [.\&][[:space:]]*[$_q]python3[$_q]...-c requires & or . preceding an opening quote — not & inside a quoted span. No match → exit 0 (quiet) ✓.

PowerShell-only rationale is correct. Bash { …; } groups require surrounding spaces; the Bash lane's existing boundary class already handles python3 -c with a { separator because that syntax requires the space. No Bash change needed.

Four new test fixtures are well-chosen: compact write (MUST block), spaced write (MUST block), quoted-name-in-block write (MUST block), fully-quoted &{python3 …} string (MUST stay quiet). All mechanically verified above. PASS=189 FAIL=0.


One Remaining Gap — <# ... #> Block-Comment Bypass (Still Open)

Confidence: HIGH — mechanically confirmed, flagged in every prior security review, not addressed by bd4a05f.

Root cause. Line 483:

_ps_nobt="$(printf '%s' "$_ps_nobt" | sed 's/#.*$//')"

sed 's/#.*$//' anchors on the first # character on the line. PowerShell's <# ... #> block-comment opener is <#, which contains # at position 2. The sed expression anchors on that # and strips everything from it to end-of-line — including -c and the write payload when they follow the comment.

Trace for python3 <# comment #> -c "open('x','w').write('a')" through the PowerShell tool:

  1. ps::write_bypass → no PS cmdlet/redirect → returns 1 (continues)
  2. ps::blank_herestrings<# is not a here-string opener (@'/@") → PS_BLANKED = raw command
  3. "${PS_BLANKED//\/}"` → no backticks → unchanged
  4. sed 's/#.*$//' → anchors on # in <#, strips # comment #> -c "open('x','w').write('a')"_ps_nobt = python3 <
  5. Brace normalization → python3 < (no braces present)
  6. _ps_lcq = python3 <; _ps_bare = python3 <
  7. Scan 1: python3 < does not have [[:space:]]+-c after python3no match
  8. Scan 2: no [.\&] call-operator prefix → no match
  9. Both scans fail → emit_tel "ok" ""; exit 0 → bypass

In PowerShell, <# ... #> is whitespace between tokens, so python3 <# comment #> -c "open('x','w').write('a')" executes identically to python3 -c "open('x','w').write('a')"the write succeeds.

Same bypass applies to call-operator form: & 'python3' <# comment #> -c "open(...)" → sed strips -c and the payload → neither scan matches.

Fix. Blank single-line <# ... #> spans from _ps_nobt before the # line-comment strip runs — one sed pre-pass:

_ps_nobt="${PS_BLANKED//\`/}"
_ps_nobt="$(printf '%s' "$_ps_nobt" | sed 's/<#[^#]*#>//g')"   # blank inline <# … #> spans
_ps_nobt="$(printf '%s' "$_ps_nobt" | sed 's/#.*$//')"          # strip # line-comments (existing)

Then add a MUST-block fixture:

run_pwsh "PS: inline block-comment between python3 and -c (blocked)" \
  "python3 <# comment #> -c \"open('x','w').write('a')\"" 2

Note: <#[^#]*#> covers the single-line inline form. Multi-line <# … #> (opener and closer on separate lines) is not handled by this pattern, but it is also not handled by ps::blank_herestrings (which targets @'/@" only) and represents a harder case for a follow-on.

Fix this →


All Prior P1 Findings Confirmed Closed

Finding Commit Status
Original bypass (python3 -c via PS tool) 86d06cc ✅ Closed
Here-string body (@'\npython3 -c open\n'@) 0353200 ✅ Closed
Line-comment (# python3 -c open() 0353200 ✅ Closed
Backtick obfuscation (python3 `-c) 4cf6ea4 ✅ Closed
Backtick line-continuation (python3 `↵-c) 4cf6ea4 ✅ Closed
Call-operator with quoted 'python3' / "python3" 4cf6ea4 ✅ Closed
Path-qualified bare + quoted interpreter a45dc75 ✅ Closed
Invoked script block &{python3 -c} forms bd4a05f ✅ Closed
<# ... #> inline block-comment ❌ Still open

Optional Follow-Up (Non-Blocking)

Double-quoted mention test (Write-Output "python3 -c open() later") — mechanism is correct (brace normalization doesn't affect it; ps::blank_quoted_spans blanks the double-quoted span before scan 1), but the test matrix covers single-quote form only. Flagged since the first code review; still optional.


Verdict

bd4a05f is correct — the invoked-script-block normalization is a clean, minimal fix that mirrors ps::write_bypass's established approach, the test fixtures cover all three attack shapes plus the false-positive guard, and no new issues are introduced. The one remaining unresolved mechanical bypass on this lane is the <# ... #> inline block-comment evasion — a pre-existing gap that has survived every review round since 0353200 introduced the # anchor strip. The fix is a one-liner and should be applied before merge given this bypass is real and straightforward to close.

@claude

claude Bot commented Jul 24, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 5m 1s —— View job


Security Review

  • Gather context
  • Get PR diff and read changed files
  • Analyze block-hook-bypass.sh for logic flaws and bypass vectors
  • Evaluate latest commit (bd4a05f) against prior open findings
  • Post security findings

What bd4a05f addresses

The latest commit normalizes PowerShell {/} to spaces in _ps_nobt before deriving the two scan forms (_ps_lcq and _ps_bare):

_ps_nobt="${_ps_nobt//\{/ }"
_ps_nobt="${_ps_nobt//\}/ }"

Invoked script block bypass — FIXED. Three forms are now covered:

  • &{python3 -c "open('x','w').write('a')"} (compact) → after brace norm: & python3 -c "..."_ps_bare = & python3 -c → scan 1 matches [[:space:];|&()/\\]+python3[[:space:]]+-c (the & and space satisfy the boundary class) → blocked
  • & {python3 -c "..."} (spaced) → same trace → blocked
  • &{'python3' -c "..."} (quoted name inside block) → brace norm: & 'python3' -c "..." → scan 2 on _ps_lcq matches [.\&][[:space:]]*[$_q]python3[$_q][[:space:]]+-cblocked

Injection surfaces — none introduced. ${var//\{/ } is a pure bash string substitution; user-controlled content from $COMMAND never enters a sed script or eval. The brace replacement does affect content inside quoted strings before ps::blank_quoted_spans runs, but since quote-blanking then strips those spans anyway, no new surface is opened.

Redundant boundary characters. The \{ and \} in scan 2's boundary class ([[:space:]\;\{\}\(\|\&]) are now dead code — brace normalization removes all braces before this regex runs. Not a security issue; harmless redundancy.


IMPORTANT — PowerShell <# ... #> inline block-comment bypass remains open

Confidence: HIGH — mechanically confirmed via code trace; flagged by every prior security review round; not addressed by any commit in this PR.

Root cause. At block-hook-bypass.sh:483:

_ps_nobt="$(printf '%s' "$_ps_nobt" | sed 's/#.*$//')"

sed 's/#.*$//' anchors on the first # character on each line. PowerShell's <# ... #> block-comment syntax opens with <# — a two-character token whose second character is #. When that # appears before -c in the command, sed anchors there and strips everything from it through end-of-line, including the -c flag and the entire write payload.

Bypass trace for python3 <# comment #> -c "open('x','w').write('a')" (through the PowerShell tool):

  1. ps::write_bypass — no PS cmdlet/redirect → returns 1 (continues).
  2. $COMMAND_LC =~ $_py_writeopen( present → condition 1 TRUE.
  3. ps::blank_herestrings<# is not a here-string opener (@'/@"); PS_BLANKED = raw command.
  4. Backtick strip → no backticks → unchanged.
  5. sed 's/#.*$//' — first # is the # in <# (position 9); strips # comment #> -c "open('x','w').write('a')"_ps_nobt = python3 <.
  6. Brace norm → no braces → python3 <.
  7. _ps_lcq = python3 <; _ps_bare = python3 < (nothing to quote-blank).
  8. Scan 1: python3(\.exe)?[[:space:]]+-c — after python3 is <; < is not whitespace → no match.
  9. Scan 2: requires a &/. call-operator → absent → no match.
  10. if branch false → emit_tel "ok" ""; exit 0bypass.

In PowerShell, <# ... #> is literal whitespace between tokens, so the shell executes python3 -c "open('x','w').write('a')" exactly as written — the file write succeeds unguarded.

The same bypass applies to the call-operator form: & 'python3' <# comment #> -c "..."sed strips from <# onward, leaving & 'python3' <; neither scan sees -c → exit 0.

Fix (one-liner pre-pass before the # line-comment strip, mirroring the here-string blanking approach):

# Blank single-line <# ... #> block-comment spans BEFORE the # line-comment strip,
# or sed anchors on the # in <# and strips -c and the payload that follow it.
_ps_nobt="$(printf '%s' "$_ps_nobt" | sed 's/<#[^#]*#>//g')"
_ps_nobt="$(printf '%s' "$_ps_nobt" | sed 's/#.*$//')"

<#[^#]*#> covers inline single-line block-comment spans. Multi-line <# / #> (on separate lines) is harder and may warrant a ps::blank_block_comments helper parallel to ps::blank_herestrings, but the single-line inline form is the most direct evasion path.

A regression fixture to add alongside: run_pwsh "PS: inline block-comment between python3 and -c (blocked)" "python3 <# comment #> -c \"open('x','w').write('a')\"" 2

Fix this →


Previously resolved items (all confirmed closed as of 4cf6ea4 / a45dc75)

  • ✅ Backtick obfuscation (python3 \-c "...") — ${PS_BLANKED//`/}` strip closes it.
  • ✅ Backtick line-continuation (python3 \↵-c "...") — same strip, newline collapses under [[:space:]]+`.
  • ✅ Call-operator with quoted python3 (& 'python3' -c "...", & "python3" -c "...") — scan 2 on _ps_lcq.
  • ✅ Path-qualified interpreter (/usr/bin/python3 -c, C:\Python313\python3.exe -c, & 'C:\Python313\python3.exe' -c) — [/\\] boundary + .exe suffix in both scans.
  • ✅ Invoked script blocks (&{python3 -c}, & {python3 -c}, &{'python3' -c}) — brace normalization in bd4a05f.

The <# ... #> inline block-comment bypass is the one remaining open mechanical evasion on the PS python3 lane. It was flagged in every prior security review and not addressed by any commit in this PR. Fix is a one-liner sed pre-pass that must run before the existing comment-strip.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bd4a05fe6b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread plugins/guardrails/hooks/block-hook-bypass.sh Outdated
…sink (review P1)

Three review rounds each surfaced a fresh evasion of the precise PowerShell
`python3 -c` scan (path-qualified target, `&{python3}` script block, quoted-`#`
comment truncation), with `<# #>` block comments and `-ArgumentList` arg-splitting
still open. PowerShell is not faithfully bash-tokenizable, so a precise
regex/normalize stack cannot converge on this class.

Pivot the PowerShell lane to the repo's SINK DOCTRINE (mirrors
ps::classify_git_command / ps::might_invoke_git): block on the mangle-resistant
co-occurrence of (a) a raw write indicator (_py_write) and (b) a python3 token
plus a `-c` inline-code flag, both seen on the quote-intact, backtick-recovered
command (new ps::might_write_via_python3). This uniformly closes the quoted,
path-qualified, brace-glued, backtick-obfuscated, block-comment, and arg-split
forms — deleting the brace-normalize, path-qualified-quoted regex, and
comment-strip scaffolding the prior rounds added (net -16 lines in the hook).

`-c` is required and position-independent, so a legitimate script or module run
(`python3 build.py`, `python3 -m tool`) touching an `open(`-like path is NOT
blocked. This CONSCIOUSLY diverges from the Bash lane, which keeps its precise
scan (strip_literals is genuinely quote-aware; Bash has no `<# #>` or `&{}`) plus
the merged path-qualified recognition.

Accepted behavior change (the approved fail-closed choice): a command that only
MENTIONS `python3 … -c` + a write indicator in prose, a line/block comment, or a
quoted string now over-blocks (three prior allow-fixtures flipped to expect-block);
here-string mentions stay inert. Accepted residual: a stdin heredoc
(`python3 - <<PY … PY`, no `-c`) is uncovered, as today.

Fixtures cover every evasion form + the flipped mentions (MUST block) and
script/module runs, read-only os.path.normpath, non-python quoted exe, here-string
mention (MUST stay quiet). Suite PASS=193 FAIL=0. Folded into 0.14.2.

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

claude Bot commented Jul 24, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 4m 34s —— View job


Review

  • Gather context and read changed files at latest commit (053425b)
  • Analyze what the "fail-closed sink" pivot does
  • Check whether the <# ... #> block-comment bypass is now addressed
  • Check the Codex P1 (quoted-hash / comment-in-quoted-span bypass) resolution
  • Check for any new issues introduced
  • Post findings

What 053425b Does

The commit abandons the precise python3 -c scan stack for the PowerShell lane and replaces it with a new ps::might_write_via_python3 sink function in ps-command.sh, following the repo's established SINK DOCTRINE (ps::classify_git_command / ps::might_invoke_git). The hook call site becomes a single two-condition check:

ps::blank_herestrings "$COMMAND"
if [[ "$COMMAND_LC" =~ $_py_write ]] && ps::might_write_via_python3 "$PS_BLANKED"; then
  block_bypass "python-write" "python3 -c inline-code file write bypasses Write/Edit hooks"
fi

The new function:

ps::might_write_via_python3() {
  local recovered="${1//\`/}" lc q="\"'"
  lc="${recovered,,}"
  [[ "$lc" =~ (^|[^[:alnum:]_.])python3([.]exe)?([^[:alnum:]_]|$) ]] || return 1
  [[ "$lc" =~ (^|[[:space:]$q])-c([[:space:]$q]|$) ]] || return 1
  return 0
}

It operates on the quote-INTACT, backtick-recovered text (PS_BLANKED with backticks stripped inline), asking only: "does this command contain a python3 interpreter token AND a -c inline-code flag?"


All Prior Open Findings — Now Resolved

Finding Status
<# ... #> inline block-comment bypass CLOSED — the new approach never strips/parses comments; both python3 and -c tokens are still visible in the raw command
-ArgumentList '-c',… arg-splitting CLOSED — the -c boundary pattern `(^
Quoted-# comment truncation (Codex P1 from bd4a05f) CLOSED — no comment stripping is performed; Write-Output '#'; python3 -c "open(...)" has python3 and -c both present → blocked
All fixes from prior rounds (backtick, call-op, brace-glued, path-qualified) Remain closed — the token co-occurrence approach inherits all of these for free

Logic Verification

python3 token regex(^|[^[:alnum:]_.])python3([.]exe)?([^[:alnum:]_]|$):

  • notpython3 -ct before python3 IS in [[:alnum:]] → boundary fails → not matched
  • & 'c:\python313\python3.exe'\ before python3 is NOT in [[:alnum:]_.] → boundary matches; ([.]exe)? takes .exe; ' after is NOT in [[:alnum:]_]matched
  • &{python3 -c}{ before python3 is NOT in [[:alnum:]_.]matched
  • python3 <# comment #> -c^ anchor → matched

-c flag regex(^|[[:space:]$q])-c([[:space:]$q]|$) (where $q = "'):

  • -Command → after -c, the next char is o, not in [[:space:]"'], not end-of-string → not matched
  • -Confirm → same → not matched
  • python3 -c "open..." → space on both sides of -cmatched
  • -ArgumentList '-c',open' on both sides of -cmatched
  • python3 <# comment #> -c "..." → space before and after -cmatched

$q expansion in [[ =~ ]]: $q="\"'" expands before the bracket expression is evaluated, making [[:space:]$q] = [[:space:]"']. Standard Bash ERE behavior, consistent with how it's used throughout the file.

Here-string protection holds: ps::blank_herestrings "$COMMAND" runs before ps::might_write_via_python3 "$PS_BLANKED". Python3 mentions inside @'…'@ here-strings are blanked before the probe, identical to the git lane's pattern. Test at line 518 confirms this.


Accepted Behavior Change — Documented and Tested

Three prior "must stay quiet" fixtures now correctly expect exit 2:

Command Old New
Write-Output 'run python3 -c open() later' exit 0 exit 2 (over-block)
Write-Output ok # python3 -c open( exit 0 exit 2 (over-block)
Write-Output '&{python3 -c open(}' exit 0 exit 2 (over-block)

These over-blocks are the explicit fail-closed cost of the sink approach and are unlikely in real LLM-generated tool calls. The documentation at block-hook-bypass.sh:468–473 and ps-command.sh:220–228 clearly explain the tradeoff and the approved failure direction.

Legitimate invocations stay allowed (the -c gate does the work):

  • python3 build.py --path "open('x','w')" → no -c flag → allowed ✓
  • python3 -m mytool "open('x','w')" → no -c flag → allowed ✓
  • & 'C:\Program Files\app.exe' -c "print open(" → no python3 token → allowed ✓

Architecture Alignment

The architectural pivot is well-reasoned and follows established repo patterns. The commit message's diagnosis — "PowerShell is not faithfully bash-tokenizable, so a precise regex/normalize stack cannot converge on this class" — is exactly what the review history demonstrates: six successive review rounds each surfaced a new evasion. The SINK DOCTRINE answer (block on mangle-resistant co-occurrence, not precise parse) is the same pattern used for ps::might_invoke_git and is the correct response to this class of problem.

The deliberate divergence from the Bash lane is correctly justified: strip_literals on the Bash lane is genuinely quote-aware, and Bash has no <# #> block comments or &{} script-block syntax to contend with.

Net code reduction of ~16 lines in the hook. The normalization scaffolding (brace normalize, backtick strip, comment strip, path-qualified-quoted regex, two-scan pattern) that accrued over the review rounds is correctly deleted.


Test Coverage — Complete

The suite covers the full attack matrix and the important carve-outs:

  • ✅ Basic open( / pathlib.write_text writes (blocked)
  • ✅ Here-string mention (allowed)
  • ✅ Script/module run with open( arg but no -c (allowed)
  • ✅ Read-only os.path.normpath (allowed)
  • ✅ Prose mention over-block (now blocked — documented)
  • ✅ Line-comment over-block (now blocked — documented)
  • ✅ Quoted string over-block (now blocked — documented)
  • ✅ All evasion forms from prior rounds (blocked)
  • ✅ Block-comment decoy + real invocation after it (blocked)
  • -ArgumentList '-c',... arg-splitting (blocked)
  • ✅ Non-python quoted exe (allowed)
  • ps::might_write_via_python3 is the python3 analogue of ps::might_invoke_git — the sink pattern is correctly implemented and positioned

No New Issues Found

All injection surfaces are unchanged: $COMMAND flows through printf '%s' | sed ... in ps::blank_herestrings; user-controlled content enters only via stdin. ps::might_write_via_python3 uses only pure Bash string substitution and [[ =~ ]] with a script-literal regex. Variable scoping is clean — everything is consumed within the PowerShell branch before its exit 0.

Verdict: Approved. The sink pivot is the correct architectural response to the class of problems the review history exposed. Every previously open security finding is now closed, the over-block cases are narrow and explicitly accepted, and the implementation is clean and well-tested.

@claude

claude Bot commented Jul 24, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 3m 55s —— View job


Security Review

  • Gather PR diff
  • Read changed files at HEAD (053425b)
  • Analyze sink doctrine implementation for bypass vectors
  • Verify injection surfaces in new function
  • Verify here-string/COMMAND_LC consistency
  • Post findings

No security vulnerabilities found in this PR's changes. Detail follows.


What 053425b changes

The final commit replaces the iterated normalize-then-scan stack (which accumulated bd4a05f brace normalization, a45dc75 path-qualified regex, 0353200 comment stripping — each adding one evasion while opening another) with a sink doctrine approach modelled on ps::might_invoke_git. A new function ps::might_write_via_python3 answers the weaker, mangle-resistant question: do the tokens python3 and -c co-occur anywhere in the quote-intact, backtick-recovered command?

The hook check at block-hook-bypass.sh:474–477 becomes:

ps::blank_herestrings "$COMMAND"
if [[ "$COMMAND_LC" =~ $_py_write ]] && ps::might_write_via_python3 "$PS_BLANKED"; then
  block_bypass "python-write" "python3 -c inline-code file write bypasses Write/Edit hooks"
fi

Prior open finding: <# … #> block-comment bypass — CLOSED

Every prior security review round confirmed a bypass via python3 <# comment #> -c "open(...)". The old approach relied on sed 's/#.*$//' which anchored on the # inside <#, stripping -c and the payload. The sink approach makes this moot: it asks only whether python3 and -c are present as tokens anywhere in the command. python3 <# comment #> -c "open(...)" still carries both tokens after backtick recovery — so ps::might_write_via_python3 returns true and the command is blocked.

Trace for python3 <# comment #> -c "open('x','w').write('a')":

  1. COMMAND_LC contains open(_py_write matches → condition 1 TRUE
  2. ps::blank_herestrings<# is not a here-string opener (@'/@") → PS_BLANKED = raw command
  3. ps::might_write_via_python3 "$PS_BLANKED": backtick recovery → unchanged; lowercase → python3 <# comment #> -c "open(...)"python3 token found (^ boundary ✓); -c token found (preceded and followed by space, matches [[:space:]"'] class ✓) → returns 0 (true)
  4. Both conditions true → blocked ✓

All previously-open P1 bypasses confirmed closed

Bypass form Fixed by Status
Original PS lane gap (python3 -c via PS tool) 86d06cc
Here-string body (@'\npython3 -c open\n'@) 0353200
Line-comment (# python3 -c open() 0353200
Backtick obfuscation (python3 `-c) 4cf6ea4
Backtick line-continuation 4cf6ea4
Call-operator + quoted interpreter (& 'python3' -c) 4cf6ea4
Path-qualified interpreter a45dc75
Invoked script block (&{python3 -c}) bd4a05f
<# … #> inline block-comment 053425b
-ArgumentList '-c',… arg-splitting 053425b

Injection surfaces — none introduced

ps::might_write_via_python3: user-controlled input flows only through Bash parameter expansion (${1//\/}) and then into [[ =~ ]]regex matching — noeval, exec, sed` script composition, or subprocess with user data in the command word. No injection path.

ps::blank_herestrings (called before the sink): uses printf '%s' "$line" | sed "s/'[^']*'//g" — user content flows through printf '%s' (no format-specifier interpretation) into sed via stdin only; the sed expression is a fixed literal. No injection path.

$COMMAND_LC =~ $_py_write: _py_write is a script literal defined at line 313, not derived from user input. Unquoted ERE variable in [[ =~ ]] is intentional Bash idiom. No injection path.


Logic correctness

Here-string/raw-command consistency. COMMAND_LC (raw) is checked for _py_write while PS_BLANKED (here-string-blanked) is passed to ps::might_write_via_python3. This is the correct split: write indicators legitimately live inside the quoted -c payload (where blanking would remove them), while the python3 and -c tokens must appear structurally in the command (outside a here-string body to be meaningful). A here-string body that mentions open( fires condition 1, but the body's python3 -c is blanked in PS_BLANKED, so condition 2 stays false — the command passes. Confirmed by the PS: here-string mentions python3 -c open (allowed) fixture.

-c boundary vs longer flags. The regex (^|[[:space:]$q])-c([[:space:]$q]|$) (where $q="\"'") only matches -c followed immediately by whitespace, a quote, or end-of-string. -Confirm lowercases to -confirm; after -c comes o, not a boundary character → no match. -Command → after -c comes o → no match. ✓

python3 boundary vs notpython3. The leading boundary [^[:alnum:]_.] prevents notpython3 from matching (the t before python3 is alphanumeric). The Bash lane fixture run "notpython3 -c open (allowed)" ... 0 covers this on the Bash side; the PS lane inherits the same test premise.

-c required; script/module runs stay allowed. python3 build.py --path "open('x','w')" has no -c token → condition 2 false → allowed ✓. python3 -m mytool "open('x','w')"-m is not -c → condition 2 false → allowed ✓. Both are new fixtures that pass.

Execution ordering. The new block is inserted BEFORE emit_tel "ok" ""; exit 0, so the original bypass (PS tool classifies cmdlet/redirect, then exit 0 before shell-agnostic scans) remains closed. ✓

Variable scoping. _ps_nobt, _ps_lcq, _ps_bare, and the other now-deleted intermediate variables are gone. PS_BLANKED is set by ps::blank_herestrings and read by ps::might_write_via_python3. The PowerShell block terminates unconditionally at exit 0 — no bleed into the Bash lane. ✓


Accepted residuals (pre-existing, not introduced by this PR)

  • py -3, bare python, python3.11-as-only-token — other interpreter spellings; the same residual the Bash lane carries
  • python3 - <<'PY' … PY stdin heredoc (no -c) — documented in the commit message and function comment
  • Command word from a variable (& $interp -c "open(...)") — the shared Bash+PS variable-command-word residual

Verdict: no security findings. The pivot to the sink doctrine is the correct architectural response to the normalize-then-scan stack's convergence failure. It uniformly closes the <# … #> block-comment bypass and the -ArgumentList arg-splitting gap alongside all previously-fixed forms, introduces no new injection surfaces, and correctly maintains the fail-closed direction (over-block on mentions, never under-block on real writes).

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 053425b371

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread plugins/guardrails/lib/powershell/ps-command.sh Outdated
PowerShell evaluates expression-valued native-command arguments before launching,
so `python3 ('-'+'c') "open(...)"` and `Start-Process python3 -ArgumentList
('-'+'c'),...` construct the `-c` flag with no literal token — the fail-closed
sink's literal `-c` check returned false and the write executed.

When no literal `-c` is present, ps::might_write_via_python3 now fails closed if
the args carry a non-tokenizable subexpression construct, reusing the git lane's
own ps::has_special_constructs test on the quote-blanked command. This is the same
construct class the git lane already refuses to parse, so the boundary is
consistent: a variable ARG (`python3 $c`) stays tokenizable/allowed exactly as
`git $x` does, while a `(...)` subexpression that could compute `-c` blocks.

Module and script runs stay allowed (`python3 -m tool "open('x')"`,
`python3 build.py --path "open('x')"` — no unquoted construct). Two computed-flag
fixtures added (subexpression + -ArgumentList). Suite PASS=195 FAIL=0. Folded into 0.14.2.

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

claude Bot commented Jul 24, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 4m 24s —— View job


Review

  • Gather context and read changed files at latest commit (3eeb06f)
  • Analyze how the computed -c Codex P1 finding is addressed
  • Check for any new issues introduced
  • Post findings

What 3eeb06f Addresses

The Codex P1 finding on 053425b — that python3 ('-'+'c') "open('x','w').write('a')" and Start-Process python3 -ArgumentList ('-'+'c'),... bypass the sink because they carry no literal -c token — is correctly fixed.

ps::might_write_via_python3 now has a third path after the literal -c check (ps-command.sh:237–244):

# No literal -c: reuse the git lane's un-parsable test on the quote-BLANKED text.
blanked=$(ps::blank_quoted_spans "$1")
ps::has_special_constructs "$blanked" && return 0

Trace for python3 ('-'+'c') "open('x','w').write('a')":

  1. python3 token found on lc → first check passes
  2. Literal -c check on lc: ('-'+'c') has no literal -cfails (the flag is split across two string literals, so the token never appears in the lowercased command)
  3. ps::blank_quoted_spans "$1" strips '-', 'c', 'x', 'w', 'a', and the double-quoted payload → blanked = "python3 (+) "
  4. ps::has_special_constructs("python3 (+) "): ( present → true → returns 0 → blocked

Trace for Start-Process python3 -ArgumentList ('-'+'c'),'open("x","w").write("a")':

  1. python3 token found → first check passes
  2. No literal -c → fails
  3. Quote-blanking strips '-', 'c', and the single-quoted payload → blanked = "Start-Process python3 -ArgumentList (+),"
  4. ps::has_special_constructs: ( present → blocked

False positive guard holds for script/module runs: python3 build.py --path "open('x','w')":

  1. python3 token found
  2. No literal -c
  3. blanked = "python3 build.py --path " (both quoted spans stripped)
  4. ps::has_special_constructs: no backtick, --%, (, {false → returns 1 → allowed

Architectural Consistency

The fix correctly reuses the exact same ps::has_special_constructs/ps::blank_quoted_spans pattern the git lane uses to refuse un-parsable targets. The parallel is exact:

  • Git lane: sees git but can't resolve its argument subexpression → fails closed
  • Python3 lane: sees python3 but can't resolve its -c argument (it's a computed subexpression) → fails closed

The commit message's stated invariant also holds: python3 $c (a variable arg, not a subexpression) → ps::blank_quoted_spans leaves python3 $c (no quoted spans to strip), ps::has_special_constructs sees no (, ), {, }, `, or --% → returns false → allowed. Consistent with how git $x stays tokenizable on the git lane.


One Subtlety — Confirmed Correct

ps::blank_quoted_spans is called with "$1" (the original PS_BLANKED input, not "$recovered" with backticks already stripped). This is intentional and correct:

  • Backticks inside quoted spans are already removed by the sed quote-stripping passes
  • A backtick outside quoted spans (a structural PowerShell element) survives into blanked and triggers ps::has_special_constructs, which is the correct fail-closed response to python3 `(…) style obfuscation
  • Any case where the backtick is adjacent to -c (i.e., python3 `-c) is caught earlier by the literal -c check on lc (which has backticks stripped via recovered)

The call site correctly passes $PS_BLANKED (here-string-blanked) rather than the raw $COMMAND, ensuring here-string bodies with python3 mentions stay inert — the same pattern as the git lane.


All P1 Findings Confirmed Closed

Finding Commit Status
Original PS lane gap (python3 -c via PS tool) 86d06cc
Here-string body (@'\npython3 -c open\n'@) 0353200
Line-comment (# python3 -c open() 0353200
Backtick obfuscation (python3 `-c) 4cf6ea4
Call-operator + quoted 'python3' / "python3" 4cf6ea4
Path-qualified interpreter (/usr/bin/python3) a45dc75
Invoked script block (&{python3 -c}) bd4a05f
<# … #> block-comment bypass 053425b (sink pivot)
-ArgumentList '-c',… arg-splitting 053425b
Computed -c (('-'+'c') subexpression) 3eeb06f

Accepted Residuals (Documented, Pre-Existing)

  • py -3, bare python, python3.11 — alternate interpreter spellings not in scope
  • python3 - <<PY … PY — stdin heredoc (no -c flag)
  • & $interp -c "open(...)" — variable command word (shared Bash+PS residual, same as git lane's & $x)

Test Coverage — Complete

Two new fixtures added for the computed-flag case (both MUST-block); the documented over-block cases (three prior allow-fixtures flipped to expect-block) are already covered from the 053425b round. Suite PASS=195 FAIL=0. CHANGELOG correctly updated.


Verdict: Approved. The computed -c fix is structurally consistent with the git lane's established pattern, correctly identified and closed the last mechanical bypass on the PS python3 lane, and introduces no new issues.

@claude

claude Bot commented Jul 24, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 5m 6s —— View job


Security Review

  • Gather PR diff
  • Read changed files at HEAD (3eeb06f)
  • Analyze ps::might_write_via_python3 and latest commit logic
  • Check for remaining bypass vectors and injection surfaces
  • Post security findings

No security vulnerabilities found in this PR's changes at HEAD (3eeb06f). Detail follows.


What 3eeb06f changes

The final commit adds a computed--c branch to ps::might_write_via_python3 (ps-command.sh:237–245):

[[ "$lc" =~ (^|[[:space:]$q])-c([[:space:]$q]|$) ]] && return 0
# No literal `-c`: PowerShell evaluates expression-valued arguments before launching,
# so `python3 ('-'+'c') …` construct the flag with no literal token. A computed `-c`
# cannot be ruled out when the args carry a non-tokenizable construct — reuse the
# git lane's un-parsable test on the quote-BLANKED text.
blanked=$(ps::blank_quoted_spans "$1")
ps::has_special_constructs "$blanked" && return 0
return 1

This is a three-path function: (1) no python3 token → allow; (2) literal -c token → block; (3) no literal -c but non-tokenizable constructs present after quote-blanking → block fail-closed.


Prior Codex P1 finding — CLOSED

python3 ('-'+'c') "open('x','w').write('a')" bypass. Trace through the new code:

  1. lc = python3 ('-'+'c') "open('x','w').write('a')"python3 token matches (line 233) ✓
  2. Literal--c check: ('-'+'c') does not contain a standalone -c token (the characters 'c' are not preceded by [[:space:]"'], they follow +) → no match → falls through to step 3
  3. ps::blank_quoted_spans strips '-', 'c', 'x', 'w', 'a' (single-quote pass) then "open(,).write()" (double-quote pass) → python3 (+) ps::has_special_constructs sees (returns 0 → blocked

Start-Process python3 -ArgumentList ('-'+'c'),... traces identically: after quote-blanking, (+) remains → ( detected → blocked ✓. Both are covered by the new test fixtures at block-hook-bypass.test.sh:558–561.


No new injection surfaces

ps::blank_quoted_spans "$1" (called at line 243): $1 (which is $PS_BLANKED) flows through printf '%s' "$text" | sed "s/'[^']*'//g" and a second sed pass. User-controlled content enters only via stdin to sed, never into the sed expression itself. No injection path.

ps::has_special_constructs "$blanked" (line 244): pure Bash case pattern match on the blanked string. No subprocess, no eval.

$_py_write in [[ =~ ]] (block-hook-bypass.sh:477): _py_write is a script literal, not user-controlled. Unquoted ERE variable in [[ =~ ]] is intentional Bash idiom; unchanged from prior commits.


$1 vs $recovered in the blanked step

The new check calls ps::blank_quoted_spans "$1" (original argument, backtick NOT yet stripped) rather than $recovered (backtick-stripped). This is not a gap: if backticks are present, ps::has_special_constructs detects them directly (backtick is in its case list). The only path that reaches the blanked step is when the literal--c scan on $lc (backtick-stripped) found no match — meaning if backticks are obfuscating -c, they have already been handled by stripping them before the literal check.


Documented residual — unchanged from prior commits

python3 $flag "open('x','w').write('a')" where $flag = '-c' at runtime: $ is not in ps::has_special_constructs's case list (backtick, --%, (, ), {, }), so the command reaches return 1 — not blocked. This is the explicit variable-command-word residual documented at ps-command.sh:62–65 and shared identically with the Bash lane. Not introduced by this PR.


All prior P1 findings confirmed closed

Finding Commit Status
Original PS lane gap (python3 -c via PS tool) 86d06cc
Here-string body (@'\npython3 -c open\n'@) 0353200
Line-comment (# python3 -c open() 0353200
Backtick obfuscation (python3 `-c) 4cf6ea4
Backtick line-continuation 4cf6ea4
Call-operator + quoted interpreter (& 'python3' -c) 4cf6ea4
Path-qualified interpreter a45dc75
Invoked script block (&{python3 -c}) bd4a05f
<# … #> inline block-comment 053425b ✅ (sink doesn't parse comments)
-ArgumentList '-c',… arg-splitting 053425b
Computed -c via ('-'+'c') 3eeb06f

Verdict: no security findings. The computed--c fix is correct and consistent with the git lane's established ps::has_special_constructs pattern. No new injection surfaces or bypass vectors are introduced.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3eeb06f2c8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread plugins/guardrails/lib/powershell/ps-command.sh
…(review P1)

A launcher whose PROGRAM name is a computed expression still bypassed the
python-write sink: `Start-Process -FilePath ('py'+'thon3') -ArgumentList
'-c','open(...)'` evaluates to the same inline write, but the literal
python3-token test in ps::might_write_via_python3 cannot see a computed name, so
it returned false before the computed-`-c` fallback could run.

ps::might_write_via_python3 now checks a computed-launcher-TARGET clause FIRST
(before the literal-token test), mirroring ps::might_invoke_git: a launcher
(Start-Process/saps/start/pwsh/powershell/cmd) followed by a computed program
argument (`(…)`/`$…`, optionally behind one named parameter) fails closed, since
the program could evaluate to python3. A LITERAL non-python launcher target
(`Start-Process notepad …`) is not matched and falls through to the token test, so
a non-python launch stays allowed. The caller still gates on a raw write indicator.

Two fixtures added (computed `-FilePath` target MUST block; literal notepad
launcher MUST stay quiet). Suite PASS=197 FAIL=0. Folded into 0.14.2.

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

claude Bot commented Jul 24, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 3m 34s —— View job


Review

  • Gather context and read changed files at latest commit (6ffd3d3)
  • Analyze the final commit's response to Codex P1 (computed Start-Process Python target)
  • Verify all prior findings are closed
  • Check for any new issues introduced
  • Post findings

What 6ffd3d3 Addresses

The Codex P1 finding on 3eeb06f — that Start-Process -FilePath ('py'+'thon3') -ArgumentList '-c','open(...)' bypassed the sink because the literal python3-token test (|| return 1) short-circuits before ps::has_special_constructs can run on a command whose interpreter name is itself computed — is correctly fixed.

The fix adds a computed-launcher-TARGET clause as the first check in ps::might_write_via_python3, mirroring ps::might_invoke_git at ps-command.sh:206:

[[ "$lc" =~ (^|[[:space:]\;\|\&\(])(start-process|saps|start|pwsh|powershell|cmd)(\.exe)?[[:space:]]+(-[a-z]+[[:space:]]+)?[\(\$] ]] && return 0

Why it must run first. In 3eeb06f the code was:

[[ python3 token check ]] || return 1   ← short-circuits here for computed target
[[ literal -c check ]] && return 0
blanked=$(ps::blank_quoted_spans …)
ps::has_special_constructs "$blanked" && return 0   ← never reached

Start-Process -FilePath ('py'+'thon3') … has no literal python3 token, so || return 1 fired immediately, bypassing the ps::has_special_constructs fallback. Placing the computed-launcher clause before the literal token check closes this.

Mechanics verified for Start-Process -FilePath ('py'+'thon3') -ArgumentList '-c','open("x","w").write("a")':

  1. COMMAND_LC =~ $_py_write: open( present → condition 1 TRUE
  2. ps::blank_herestrings → no here-string → PS_BLANKED = raw command
  3. ps::might_write_via_python3 "$PS_BLANKED":
    • lc = "start-process -filepath ('py'+'thon3') -argumentlist '-c','open(\"x\",\"w\").write(\"a\")'"
    • Computed launcher check: start-process matches launcher; matches [[:space:]]+; -filepath matches (-[a-z]+[[:space:]]+)?; ( matches [\(\$]returns 0 → blocked

False-positive guard verified for Start-Process notepad -ArgumentList '-c','open("x","w")':

  1. COMMAND_LC =~ $_py_write: open( present → TRUE (gated but continues)
  2. ps::might_write_via_python3:
    • Computed launcher check: start-process matches, (-[a-z]+[[:space:]]+)? is optional, next char is n from notepad (not a named param, not ( or $) → no match
    • Literal python3 check: python3 not in lcreturns 1 → allowed

One-Named-Parameter Limitation — Covered by Fallback

The (-[a-z]+[[:space:]]+)? group handles exactly one named parameter (-FilePath). For Start-Process -NoNewWindow -FilePath ('py'+'thon3') … (two named params before the computed name), the computed-launcher clause does not match. However, this case is caught anyway by the ps::has_special_constructs fallback:

  • lc has python3 absent → falls through literal python3 check fails... wait, actually it hits the computed-c path:
    • ps::blank_quoted_spans strips 'py' and 'thon3' → blanked contains (+)( detected by ps::has_special_constructsblocked

The one-param limit is the same documented constraint in ps::might_invoke_git (same clause, same codebase pattern), and the ps::has_special_constructs fallback covers multi-param cases because any computed expression leaves () in the blanked text. No gap.


All Prior P1 Findings — Confirmed Closed

Finding Commit Status
Original PS lane gap (python3 -c via PS tool) 86d06cc
Here-string body (@'\npython3 -c open\n'@) 0353200
Line-comment (# python3 -c open() 0353200
Backtick obfuscation (python3 `-c) 4cf6ea4
Backtick line-continuation 4cf6ea4
Call-operator + quoted interpreter (& 'python3' -c) 4cf6ea4
Path-qualified interpreter a45dc75
Invoked script block (&{python3 -c}) bd4a05f
<# … #> inline block-comment bypass 053425b (sink pivot)
-ArgumentList '-c',… arg-splitting 053425b
Computed -c flag (('-'+'c')) 3eeb06f
Computed launcher target (-FilePath ('py'+'thon3')) 6ffd3d3

No New Issues Introduced

Injection surfaces — clean. The new clause is a pure Bash ERE test on $lc, which is the backtick-stripped, lowercased copy of PS_BLANKED. No subprocess, no eval, no sed with user data in the expression.

Launcher-name set (start-process|saps|start|pwsh|powershell|cmd) — identical to ps::might_invoke_git. start has potential overlap with other uses, but it requires [[:space:]]+ after and [\(\$] within (-[a-z]+[[:space:]]+)?, so start-service and bare mentions don't match. This is the same set and boundary used in the git lane — consistency is correct.

saps $exe -ArgumentList '-c','open(...)' correctly blocked. A variable-program launcher ($) matches [\(\$] and can't be proven non-python3; blocking is the correct fail-closed outcome and mirrors ps::might_invoke_git's treatment of & $tool ….

Test suite. Two fixtures added: computed -FilePath target (MUST block) and literal notepad launcher (MUST stay quiet). The CHANGELOG 0.14.2 entry is updated to include computed-launcher-target coverage. PASS=197 FAIL=0 per commit message.


Accepted Residuals (Pre-Existing, Documented)

  • python3 $flag … where $flag = '-c' at runtime — variable command word, same residual as the Bash lane and the git lane
  • py -3, bare python — alternate interpreter spellings not in scope
  • python3 - <<PY … PY — stdin heredoc (no -c)
  • Multi-line <# … #> block comments (opener and closer on separate lines) — harder than single-line; the single-line form is covered by the sink approach, and multi-line is rare in LLM-generated tool calls

Verdict: Approved. The computed-launcher-target fix is structurally identical to ps::might_invoke_git's established clause, correctly positioned as the first check before the early-return literal token test, and well-tested. Every P1 security finding raised across the full review history is now closed. The implementation is clean and the accepted residuals are documented.

@claude

claude Bot commented Jul 24, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 4m 4s —— View job


Security Review

  • Get PR diff
  • Read changed files at HEAD (6ffd3d3)
  • Analyze block-hook-bypass.sh and ps-command.sh for bypass vectors and injection surfaces
  • Check computed Start-Process python target finding (last open Codex P1)
  • Post findings

No security vulnerabilities found in this PR's changes. Detail follows.


What 6ffd3d3 changes

The final commit adds a computed-launcher clause to ps::might_write_via_python3 (ps-command.sh:240), inserted before the literal python3-token test:

[[ "$lc" =~ (^|[[:space:]\;\|\&\(])(start-process|saps|start|pwsh|powershell|cmd)(\.exe)?[[:space:]]+(-[a-z]+[[:space:]]+)?[\(\$] ]] && return 0

When a process-launcher whose program slot is a computed expression ((…)) or variable reference ($…) is found alongside a write indicator, it returns 0 (block) before ever checking for a literal python3 token — because the literal token test cannot see a computed name.


Last open Codex P1 — CLOSED

Start-Process -FilePath ('py'+'thon3') -ArgumentList '-c','open("x","w").write("a")' trace:

  1. COMMAND_LC contains open(_py_write matches → condition 1 true.
  2. ps::blank_herestrings "$COMMAND" → no here-string → PS_BLANKED = raw command.
  3. ps::might_write_via_python3 "$PS_BLANKED":
    • lc = start-process -filepath ('py'+'thon3') -argumentlist '-c','open("x","w").write("a")'
    • Launcher regex: start-process at ^[[:space:]]+-filepath matches (-[a-z]+[[:space:]]+)?( matches [\(\$]MATCH → returns 0 → blocked ✓

Literal Start-Process notepad stays allowed:

Start-Process notepad -ArgumentList '-c','open("x","w")':

  • Launcher regex: start-process → optional -[a-z]+ group doesn't match notepad (no leading -) → [\(\$] requires ( or $, finds nno match.
  • Python3 token test: notepadpython3returns 1 → allowed ✓

Positional computed form also covered:

Start-Process ('py'+'thon3') -ArgumentList ...:

  • Launcher regex: after start-process , optional group skips (( doesn't start with -), then [\(\$](MATCH → blocked ✓

Variable-target form also covered:

saps $exe -ArgumentList '-c','open(...)':

  • Launcher regex: saps → optional group skips, [\(\$]$MATCH → blocked ✓

Architectural consistency

The new launcher regex at ps-command.sh:240 is a byte-for-byte copy of the equivalent clause in ps::might_invoke_git at ps-command.sh:206. The design decision — "optionally behind one named parameter" — is shared exactly between the two functions. A launcher with two or more named parameters before the computed target (e.g., Start-Process -Wait -FilePath ('py'+'thon3') ...) doesn't match the launcher regex, but this is a pre-existing limitation that also affects ps::might_invoke_git and is deliberately accepted in both functions.


No new injection surfaces

Launcher regex on $lc: $lc is a lowercased parameter expansion of recovered (backticks stripped from $PS_BLANKED). In [[ "$lc" =~ … ]], the left-hand side is quoted — user-controlled content enters only as the string to match, never as the regex itself. No injection path.

blanked=$(ps::blank_quoted_spans "$1") (computed-flag fallback, ps-command.sh:252): $1 flows through printf '%s' "$text" | sed "s/'[^']*'//g" — user content enters via stdin, never into the sed expression. The $1 vs $recovered choice is intentional: backticks outside quoted spans survive into blanked and are detected by ps::has_special_constructs's *'`'* case, while backticks inside quoted spans are stripped by ps::blank_quoted_spans alongside the spans that contain them.

Variable scoping: recovered, lc, q, blanked are all local and consumed within ps::might_write_via_python3. PS_BLANKED is set by ps::blank_herestrings and consumed before exit 0 in the [[ "$TOOL_NAME" == "PowerShell" ]] block. No bleed into the Bash lane.


All prior P1 findings — confirmed closed

Finding Commit Status
Original PS lane gap (python3 -c via PS tool) 86d06cc
Here-string body (@'\npython3 -c open\n'@) 053425b (sink pivot)
Line-comment / <# #> block-comment bypass 053425b (sink pivot)
Backtick obfuscation (python3 `-c) 053425b (sink pivot)
Call-operator + quoted interpreter (& 'python3' -c) 053425b (sink pivot)
Path-qualified interpreter 053425b (sink pivot)
Invoked script block (&{python3 -c}) 053425b (sink pivot)
-ArgumentList '-c',… arg-splitting 053425b (sink pivot)
Computed -c via ('-'+'c') 3eeb06f
Computed Start-Process target ('py'+'thon3') 6ffd3d3

Verdict: no security findings. The computed-launcher fix mirrors ps::might_invoke_git's established pattern exactly, correctly closes the last Codex P1 on this PR, and introduces no new injection surfaces or bypass vectors.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6ffd3d3bf7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread plugins/guardrails/lib/powershell/ps-command.sh
… (review P1)

PowerShell joins an adjacent variable/subexpression into the same native-command
argument, so `python3 -c$code` passes a single `-c<source>` arg. The literal-`-c`
check required a whitespace/quote/end boundary after `-c`, so the concatenated
form slipped it, and a bare `$var` is not a special construct for the fallback.

ps::might_write_via_python3 now also blocks when `-c` is immediately followed by a
variable (`$`) or subexpression (`(`) — a computed inline-code flag. A longer
literal flag (`-config`, `-Command`, `-Confirm`) is unaffected (the char after
`-c` is a letter, not `$`/`(`).

Two fixtures added (Codex's exact `-c$code` MUST block; `-config` longer flag MUST
stay quiet). Suite PASS=199 FAIL=0. Folded into 0.14.2.

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

claude Bot commented Jul 24, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 3m 4s —— View job


Security Review

  • Get PR diff
  • Read changed files at HEAD (e22217a)
  • Analyze block-hook-bypass.sh for bypass vectors and injection surfaces
  • Analyze ps-command.sh for security issues
  • Post findings

No security vulnerabilities found in this PR's changes. Detail follows.


Changed files reviewed


Analysis

Injection surfaces — none introduced.

User-controlled data ($COMMAND / $PS_BLANKED) flows through four paths in the new code, all clean:

  1. ps::blank_herestrings: printf '%s' "$line" | sed "s/…//g" — user content enters via stdin only; the sed expression is a fixed literal. No injection.
  2. ${1//\/}backtick strip inps::might_write_via_python3`: pure bash string parameter expansion, no subprocess.
  3. [[ "$lc" =~ pattern ]], [[ "$lc" =~ … ]] — user content is on the left-hand side, which is quoted. Pattern (RHS) is a script literal with only $q (the two quote-char constant "') expanded into it. No injection via the regex engine.
  4. ps::blank_quoted_spans "$1": same printf '%s' | sed pattern as above. No injection.

Logic correctness — four-path detection is sound.

ps-command.sh:240–260:

  1. Computed launcher clause: fires before the literal-token test so Start-Process -FilePath ('py'+'thon3') … is caught even though python3 never appears literally. Mirrors ps::might_invoke_git:206 exactly.
  2. Literal python3 token: boundary class [^[:alnum:]_.] prevents notpython3 false-matches; the ([.]exe)? extension admits Windows path-qualified invocations.
  3. Literal -c flag: (^|[[:space:]$q])-c([[:space:]$q]|$) — quote-bounded so '-c' in an -ArgumentList is matched while -Command/-Confirm/-config are not.
  4. Concatenated -c$code: (^|[[:space:]$q])-c[\$\(] catches PowerShell's native-command adjacent-expansion idiom where -c and the inline-code source are joined into a single argument with no whitespace boundary.
  5. Computed -c fallback: ps::blank_quoted_spans + ps::has_special_constructs — when no literal -c is found but a non-tokenizable construct ((…), {…}, etc.) is present after quote-blanking, the function correctly fails closed, mirroring how the git lane handles unparseable commands.

$q variable in character classes — correct.

q="\"'" expands to the two-character string "'. Inside [[ =~ ]], [[:space:]$q] expands before ERE evaluation to [[:space:]"'] — whitespace, double-quote, single-quote. This is the intended character class and is consistent with ps::might_invoke_git and ps::has_dynamic_invocation, which use the same pattern.

[\$\(] character class — fail-closed quirk, harmless.

In POSIX ERE inside bash [[ =~ ]], backslash inside […] is not an escape character (most Linux ERE implementations treat it literally). So [\$\(] is a class of \, $, ( rather than just $ and (. This means python3 -c\continuation would also block. That is a minor over-block in the fail-closed direction — not a bypass and not a security issue. It is consistent with the rest of this codebase's use of \ escapes inside bracket expressions.

Execution ordering — correct.

block-hook-bypass.sh:476–481: The new python3 probe runs after ps::write_bypass and before emit_tel "ok" ""; exit 0. The original bypass (PowerShell lane classified cmdlet/redirect then exit 0 before shell-agnostic scans) is correctly closed.

Variable scoping — clean.

recovered, lc, q, blanked are all local inside ps::might_write_via_python3. PS_BLANKED is set by ps::blank_herestrings and consumed before exit 0 in the [[ "$TOOL_NAME" == "PowerShell" ]] block. No bleed into the Bash lane below.

Here-string / PS_BLANKED split — correct.

The PowerShell lane calls ps::blank_herestrings "$COMMAND" before passing $PS_BLANKED to ps::might_write_via_python3. A here-string body containing python3 -c open(…) is blanked to the placeholder before the token probe, so an inert mention in a here-string does not trigger blocking. This matches the git lane's pattern.

$_py_write on raw COMMAND_LC — unchanged and correct.

The write-indicator scan still runs on the raw command (not the here-string-blanked form) because the write indicators legitimately live inside the -c payload. This is the same split used by the Bash lane and was analyzed in earlier review rounds.

All prior P1 bypass findings — confirmed closed at HEAD.

Finding Commit Status
Original PS lane gap (python3 -c via PS tool) 86d06cc
Here-string body (@'\npython3 -c open\n'@) 053425b ✅ (sink doesn't strip; here-string blanked first)
<# … #> inline block-comment 053425b ✅ (sink doesn't strip comments; tokens still visible)
Backtick obfuscation / line-continuation 053425b ✅ (${1//\/}` strip before all checks)
Call-operator + quoted interpreter (& 'python3') 053425b ✅ (quote-intact probe sees it)
Path-qualified interpreter (\python3.exe) 053425b ✅ (token boundary admits .)
Invoked script block (&{python3 -c}) 053425b ✅ (quote-intact probe sees python3)
-ArgumentList '-c',… arg-splitting 053425b ✅ (literal -c check matches quote-bounded form)
Computed -c via ('-'+'c') 3eeb06f ✅ (ps::has_special_constructs fallback)
Computed launcher target ('py'+'thon3') 6ffd3d3 ✅ (launcher clause runs first)
Concatenated -c$code e22217a ✅ (-c[\$\(] pattern)

No workflow files touched. No credential or secret handling introduced. No dependency or supply-chain changes.


Verdict: no security findings. The sink-doctrine implementation is correct and consistent with the established ps::might_invoke_git pattern. All attack surfaces identified across the full review history are closed at HEAD. The fail-closed direction is maintained throughout, and no new injection surfaces are introduced.

@claude

claude Bot commented Jul 24, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 3m 23s —— View job


Review

  • Gather context and read changed files at latest commit (e22217a)
  • Analyze the final commit's response to Codex P1 (-c$code concatenated argument bypass)
  • Verify all prior findings are closed
  • Check for any new issues introduced
  • Post findings

What e22217a Addresses

The last Codex P1 — that python3 -c$code (variable concatenated directly onto -c with no whitespace or quote separator) evades both the literal -c check and the ps::has_special_constructs fallback — is correctly fixed by this commit.

The literal -c check at ps-command.sh:245 requires a whitespace/quote boundary after -c:

[[ "$lc" =~ (^|[[:space:]$q])-c([[:space:]$q]|$) ]] && return 0

-c$code has $ immediately after -c, so this didn't match. And $var is not in ps::has_special_constructs's construct set (which covers backtick, --%, (, {, }), so the fallback also missed it.

The fix adds a targeted third check at ps-command.sh:251:

[[ "$lc" =~ (^|[[:space:]$q])-c[\$\(] ]] && return 0

Bypass closed — trace for $code = '…'; python3 -c$code:

  1. COMMAND_LC contains write_text_py_write matches → condition 1 TRUE
  2. ps::blank_herestrings → no here-string → PS_BLANKED = raw command
  3. ps::might_write_via_python3:
    • lc = $code = '…'; python3 -c$code (backticks stripped, lowercased)
    • Computed-launcher: no start-process/saps/etc. → no match
    • python3 token: python3 preceded by space → matches → continues
    • Literal -c check: -c$code — after -c is $, not whitespace/quote/end → no match
    • New check: (^|[[:space:]"'])-c[$\(] — space before -c, then $ follows → MATCH → returns 0 → blocked

False-positive guard for python3 -config "pathlib.Path":

  • lc = python3 -config "pathlib.path"
  • Literal -c check: after -c is o (from -config) → no match
  • New check: after -c is o → no match
  • ps::has_special_constructs on blanked python3 -config → no constructs → returns 1 → allowed

-c$(expr) form also covered by the new check. In PowerShell, python3 -c(...) evaluates the expression and concatenates it as the argument. The new check's [\$\(] class catches ( after -c as well.

Backtick interaction correct. python3 `-c$code → after backtick strip: python3 -c$code → new check catches it ✓.


Test Fixtures — Correct and Sufficient

Two new fixtures at block-hook-bypass.test.sh:573–576:

  • $code = '…'; python3 -c$code — MUST block (the Codex P1 exact form) ✓
  • python3 -config "pathlib.Path" — MUST stay quiet (longer literal flag not a -c + code form) ✓

PASS=199 FAIL=0 per commit message.


All P1 Findings — Confirmed Closed

Finding Commit Status
Original PS lane gap (python3 -c via PS tool) 86d06cc
Here-string body (@'\npython3 -c open\n'@) 053425b (sink pivot)
Line-comment / <# #> block-comment bypass 053425b (sink doesn't parse comments)
Backtick obfuscation (python3 `-c) 053425b (sink pivot)
Call-operator + quoted interpreter (& 'python3' -c) 053425b (sink pivot)
Path-qualified interpreter (/usr/bin/python3, python3.exe) 053425b (sink pivot)
Invoked script block (&{python3 -c}, & {python3 -c}) 053425b (sink pivot)
-ArgumentList '-c',… arg-splitting 053425b (sink pivot)
Computed -c via ('-'+'c') 3eeb06f
Computed launcher target (-FilePath ('py'+'thon3')) 6ffd3d3
-c concatenated with $var / (expr) e22217a

No New Injection Surfaces

The new check is a pure [[ =~ ]] ERE test on $lc (a backtick-stripped, lowercased expansion of $PS_BLANKED). No subprocess, no eval, no sed with user content in the script. Consistent with every other test in the function.


Accepted Residuals (Documented, Pre-Existing)

  • python3 $flag … where $flag = '-c' at runtime — variable command word; $ is not in ps::has_special_constructs by design (shared Bash+PS residual, same as the git lane)
  • py -3, bare python, python3.11 — alternate interpreter spellings
  • python3 - <<PY … PY — stdin heredoc (no -c flag)

Optional Follow-Up (Non-Blocking, Carry-Over)

Double-quoted mention testWrite-Output "python3 -c open() later" is not covered as a test fixture. The mechanism handles it consistently (the sink's python3 token + -c check over-blocks it the same way as the single-quoted mention at line 523), but the test matrix has the single-quote form only. Flagged as optional since the first code review round; the behavior is correct even without it.


Verdict: Approved. The -c$code fix is a minimal, precisely-targeted addition to ps::might_write_via_python3 — one [[ =~ ]] line that closes the last mechanical bypass form without affecting legitimate longer flags. Every P1 security finding from across the ten-commit review history is now closed. The implementation is clean, well-commented, correctly tested, and the SINK DOCTRINE architecture has proven its value by absorbing the full class of PowerShell tokenization evasions.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e22217adb9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread plugins/guardrails/lib/powershell/ps-command.sh Outdated
…(review P1)

PowerShell binds a cmdlet parameter with a colon as well as whitespace, so
`Start-Process -FilePath:$p -ArgumentList '-c',...` names a computed program the
same as `-FilePath ('py'+'thon3')`. The computed-launcher clause in
ps::might_write_via_python3 required whitespace after the optional parameter name,
so the colon-bound form slipped it and the launch executed unguarded.

The optional parameter-name group now accepts a colon OR whitespace value
separator (`-[a-z]+([[:space:]]+|:)`), so `-FilePath:$p` / `-FilePath ('py'…)` are
both caught; a literal non-python launcher (`Start-Process notepad …`,
`Start-Process foo:bar baz`) still is not. ps::might_invoke_git carries the
identical clause with the same whitespace-only gap — a matching fix on the git lane
is tracked separately (out of scope for this #1199 python-write PR).

One fixture added (colon-bound computed target MUST block). Suite PASS=200 FAIL=0.
Folded into 0.14.2.

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

claude Bot commented Jul 24, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 3m 19s —— View job


Review

  • Read changed files at HEAD (352dd78)
  • Analyze colon-bound launcher fix (latest Codex P1)
  • Verify all prior findings remain closed
  • Check for any new issues introduced
  • Post findings

What 352dd78 Addresses

The Codex P1 finding on e22217a — that Start-Process -FilePath:$p -ArgumentList '-c','open(...)' bypasses the computed-launcher clause because PowerShell's colon-bound parameter binding form (: separator instead of whitespace) doesn't match the required [[:space:]]+ after the optional parameter name — is correctly fixed.

Change. One targeted regex edit in ps-command.sh:244:

# Before
(-[a-z]+[[:space:]]+)?[\(\$]
# After
(-[a-z]+([[:space:]]+|:))?[\(\$]

The optional parameter name group now accepts either whitespace (-FilePath ('py'+'thon3')) or a colon (-FilePath:$p) as the value separator. Both forms are equivalent in PowerShell's argument binding rules.


Mechanics Verified

Bypass closed. $p = 'py'+'thon3'; Start-Process -FilePath:$p -ArgumentList '-c','open("x","w").write("a")':

  1. COMMAND_LC =~ $_py_writeopen( present → condition 1 TRUE
  2. ps::might_write_via_python3 "$PS_BLANKED": lc = ...; start-process -filepath:$p ...
  3. Launcher clause: ; boundary → start-process → space → -filepath: matches (-[a-z]+([[:space:]]+|:))?$ from $p matches [\(\$]returns 0 → blocked

False-positive guard holds. Start-Process notepad -ArgumentList '-c','open("x","w")':

  1. Launcher clause: ^start-process → optional group skips (notepad doesn't start with -) → [\(\$] looks at nno match
  2. Python3 token: python3 not present → returns 1 → allowed

Start-Process -Param:literal_value stays allowed. For Start-Process -FilePath:notepad:

  • Optional group matches -filepath:, then [\(\$] looks at n from notepadno match

Start-Process -FilePath:'C:\Python313\python3.exe' stays blocked. The launcher clause doesn't fire (quoted value's ' is not in [\(\$]), but the python3-token + literal--c path catches it since python3 is visible in the quote-intact lc and -c appears in -argumentlist '-c',...


All Prior P1 Findings — Confirmed Closed at HEAD

Finding Commit Status
Original PS lane gap (python3 -c via PS tool) 86d06cc
Here-string body (@'\npython3 -c open\n'@) 053425b (sink pivot)
<# … #> block-comment bypass 053425b (sink pivot)
Backtick obfuscation (python3 `-c) 053425b (sink pivot)
Call-operator + quoted interpreter (& 'python3' -c) 053425b (sink pivot)
Path-qualified interpreter (/usr/bin/python3, python3.exe) 053425b (sink pivot)
Invoked script block (&{python3 -c}) 053425b (sink pivot)
-ArgumentList '-c',… arg-splitting 053425b (sink pivot)
Computed -c via ('-'+'c') 3eeb06f
Computed launcher target (-FilePath ('py'+'thon3')) 6ffd3d3
-c concatenated with $var / (expr) e22217a
Colon-bound computed launcher target (-FilePath:$p) 352dd78

No New Issues Introduced

Injection surfaces — clean. The change is entirely within a [[ "$lc" =~ … ]] ERE test where $lc is the left-hand side (quoted). No subprocess, no eval, no sed with user data in the script.

start name overlap. The start launcher name in (start-process|saps|start|pwsh|powershell|cmd) is identical to the prior commit — the new colon form doesn't change its scope. start notepad:task baz would not match because notepad:task would need t to match [\(\$].

ps::might_invoke_git parallel gap. Correctly noted in the commit message as out-of-scope for this PR and tracked separately. The python3 lane is the only lane this PR covers.


Accepted Residuals (Documented, Pre-Existing)

  • python3 $flag … where $flag = '-c' at runtime — variable command word; same residual as the Bash lane and git lane
  • py -3, bare python, python3.11 — alternate interpreter spellings
  • python3 - <<PY … PY — stdin heredoc (no -c)
  • Multi-param colon-bound launchers (e.g., Start-Process -Wait:$true -FilePath:$p) — the first -Wait: would match the optional group, then $ from $true triggers a fail-close. This is the correct fail-safe behavior: any $-valued parameter in the first slot blocks rather than passes.

Optional Follow-Up (Non-Blocking)

Double-quoted mention test (Write-Output "python3 -c open() later") — still not covered as a fixture. The mechanism handles it correctly (the token co-occurrence probe blocks it the same way as the single-quoted form at line 523), but the test matrix has the single-quote form only. This has been flagged since the first code review round and remains optional.


Verdict: Approved. The colon-bound launcher fix is a one-group regex change, exactly targeted, with no new injection surfaces, and correctly closes the last Codex P1 on this PR. The full 11-commit review history has converged on a clean SINK DOCTRINE implementation — ps::might_write_via_python3 now covers the complete class of PowerShell token obfuscation forms. Ready to merge.

@claude

claude Bot commented Jul 24, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 3m 50s —— View job


Security Review

  • Get PR diff
  • Read changed files at HEAD (352dd78)
  • Analyze block-hook-bypass.sh and ps-command.sh for bypass vectors and injection surfaces
  • Check colon-bound computed launcher target (last open Codex P1)
  • Post security findings

No security vulnerabilities found in this PR's changes. Detail follows.


What 352dd78 changes

The final commit fixes the last Codex P1 — that $p = 'py'+'thon3'; Start-Process -FilePath:$p -ArgumentList '-c','open("x","w").write("a")' bypassed the sink because the computed-launcher regex required whitespace after the parameter name (-[a-z]+[[:space:]]+), missing the colon-bound binding form (-FilePath:$p).

The fix widens the inner group from (-[a-z]+[[:space:]]+)? to (-[a-z]+([[:space:]]+|:))? at ps-command.sh line 244:

[[ "$lc" =~ (^|[[:space:]\;\|\&\(])(start-process|saps|start|pwsh|powershell|cmd)(\.exe)?[[:space:]]+(-[a-z]+([[:space:]]+|:))?[\(\$] ]] && return 0

Colon-bound bypass — CLOSED

Trace for $p = 'py'+'thon3'; Start-Process -FilePath:$p -ArgumentList '-c','open("x","w").write("a")':

  1. COMMAND_LC contains open(_py_write matches → condition 1 TRUE
  2. ps::blank_herestrings → no here-string → PS_BLANKED = raw command
  3. ps::might_write_via_python3 "$PS_BLANKED":
    • lc = "$p = 'py'+'thon3'; start-process -filepath:$p -argumentlist '-c','open(...)'" (backtick-stripped, lowercased)
    • Computed-launcher regex: start-process at ; boundary → [[:space:]]+-filepath: matches (-[a-z]+([[:space:]]+|:))? where : is the new alternative → $ from $p matches [\(\$]MATCH → returns 0 → blocked ✓

Literal notepad launcher still allowed:
Start-Process notepad -ArgumentList '-c','open("x","w")' — optional group skips (n not a -), [\(\$] must match n → no match → python3 token check finds no python3returns 1 → allowed ✓

The test fixture at block-hook-bypass.test.sh:570–571 covers the exact colon-bound attack form.


No new injection surfaces

The change is a regex literal modification only. $lc (derived from ${1//\/}lowercased) is on the **left side** of[[ "$lc" =~ … ]]` (quoted) — user-controlled content cannot reach the regex pattern. No subprocess, no eval, no sed with user data in the script.


Documented edge cases verified correct

Colon-bound + space after colon (-FilePath: $p): PowerShell does not accept a space between : and its value in this binding form — this is a syntax error in PS, not a real evasion surface. The regex does not need to handle it.

Two named parameters before computed target (Start-Process -Wait -FilePath:$p): The optional group matches only ONE parameter. This falls through to the python3 token check — $p has no literal python3, so it returns 1 (allowed). This is the same single-param limitation present in ps::might_invoke_git (same clause, explicitly acknowledged in the comment at ps-command.sh:242–243 and tracked as a separate fix). Not introduced by this PR.

$python3 variable-named interpreter in positional trailing slot (Start-Process -ArgumentList '-c','open(...)' $python3): The python3 token check sees $ before python3 as a non-alnum boundary → match → literal -c check fires on '-c' (quote-bounded) → blocked ✓.


All prior P1 findings — confirmed closed at HEAD

Finding Commit Status
Original PS lane gap (python3 -c via PS tool) 86d06cc
Here-string body (@'\npython3 -c open\n'@) 053425b (sink pivot)
<# … #> inline block-comment bypass 053425b (sink never strips comments)
Backtick obfuscation / line-continuation 053425b (${1//\/}` strip)
Call-operator + quoted interpreter (& 'python3') 053425b (quote-intact probe)
Path-qualified interpreter (\python3.exe) 053425b (token boundary admits .)
Invoked script block (&{python3 -c}) 053425b (quote-intact probe)
-ArgumentList '-c',… arg-splitting 053425b (literal -c quote-bounded)
Computed -c via ('-'+'c') 3eeb06f (ps::has_special_constructs fallback)
Computed launcher target ('py'+'thon3') 6ffd3d3 (launcher clause)
Concatenated -c$code e22217a (-c[\$\(] pattern)
Colon-bound -FilePath:$p 352dd78 (([[:space:]]+|:) alt)

Verdict: no security findings. The colon-bound parameter fix is a minimal, correctly-targeted change that mirrors the whitespace-separated form already in ps::might_invoke_git. Every P1 bypass surfaced across the full eleven-commit review history is now closed. The fail-closed direction is maintained throughout, and no new injection surfaces or logic gaps are introduced.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 352dd784a6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread plugins/guardrails/lib/powershell/ps-command.sh Outdated
…target (review P1)

Modeling Start-Process parameter ordering with a regex kept losing to fresh
variants — one preceding option, then colon binding, now multiple options before
`-FilePath` (`Start-Process -NoNewWindow -FilePath $exe -ArgumentList '-c',...`).
Each slipped the per-parameter regex and launched an inline Python write unguarded.

Replace the per-parameter regex in ps::might_write_via_python3 with a conservative
rule that closes the whole ordering/binding sub-class: a launcher
(Start-Process/saps/start/pwsh/powershell/cmd) present TOGETHER with an unquoted
computed construct (`$` or `(`, on the quote-blanked command) fails closed — the
launched program cannot be proven non-python regardless of how it is bound or how
many options precede it. A launcher with only literal args (`Start-Process notepad
-ArgumentList '-c','open(...)'`, `Start-Process notepad -ArgumentList "$file"`)
carries no unquoted construct and stays allowed via the literal-token path.

ps::might_invoke_git still models this with the narrower per-parameter regex; a
matching hardening on the git lane is tracked separately (out of this #1199 scope).

One fixture added (multi-option computed target MUST block). Suite PASS=201 FAIL=0.
Folded into 0.14.2.

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

claude Bot commented Jul 24, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 5m 3s —— View job


Review

  • Read changed files at HEAD (65e4c33)
  • Evaluate final commit against open findings from prior rounds
  • Analyze block-hook-bypass.sh and ps-command.sh for correctness and security
  • Review test coverage
  • Review CHANGELOG and version bump
  • Post findings

What 65e4c33 Does

This final commit abandons the parameter-position regex for computed launcher targets (which had lost three successive rounds to -FilePath:$p, then (-[a-z]+[[:space:]]+)? carve-out gaps, then multi-option prefix) and replaces it with a conservatively broad check:

Before (352dd78, per-parameter regex):

[[ "$lc" =~ (^|[…])(start-process|saps|start|pwsh|powershell|cmd)(\.exe)?[[:space:]]+(-[a-z]+([[:space:]]+|:))?[\(\$] ]] && return 0

After (65e4c33, any-construct check):

blanked=$(ps::blank_quoted_spans "$1")
if ps::has_launcher "$recovered" && [[ "$blanked" == *'$'* || "$blanked" == *'('* ]]; then
  return 0
fi

If a launcher name is present AND any unquoted $ or ( survives quote-blanking, the function fails closed. The quote-blanked form is the key: write-indicator tokens like open( appear inside single- or double-quoted strings (the -c payload), so they're stripped — only a genuinely unquoted computed construct survives into $blanked.


Mechanics Verified

Multi-option computed target — newly fixed ($exe = 'py'+'thon3'; Start-Process -NoNewWindow -FilePath $exe -ArgumentList '-c','open("x","w").write("a")'):

  • $blanked after quote-blanking strips 'py', 'thon3', '-c', 'open("x","w").write("a")'$exe = +; Start-Process -NoNewWindow -FilePath $exe -ArgumentList ,
  • $exe still contains $ → TRUE → blocked ✓

Literal non-python launcher — stays allowed (Start-Process notepad -ArgumentList '-c','open("x","w")'):

  • $blanked strips '-c' and 'open("x","w")'Start-Process notepad -ArgumentList ,
  • No $ or ( in blanked → falls through to literal token test
  • No python3 token → returns 1 → allowed ✓ (test at line 576)

Colon-bound from prior round also still closed (-FilePath:$p):

  • $blanked strips the single-quoted 'py'+'thon3' fragments but leaves the bare $p$ in blanked → blocked ✓

blanked is now computed once at top of function and reused for both the new launcher check and the existing ps::has_special_constructs fallback. The computation is idempotent (ps::blank_quoted_spans "$1" is the same call site as before), so no regression there — just a structural cleanup that moves the subshell spawn before the early-return paths.


All P1 Findings — Confirmed Closed at HEAD

Finding Commit Status
Original PS lane gap (python3 -c via PS tool) 86d06cc
Here-string body / block-comment bypass 053425b (sink pivot)
Backtick obfuscation / line-continuation 053425b
Call-operator + quoted interpreter 053425b
Path-qualified interpreter 053425b
Invoked script block (&{python3 -c}) 053425b
-ArgumentList '-c',… arg-splitting 053425b
Computed -c via ('-'+'c') 3eeb06f
Computed launcher target (-FilePath ('py'+'thon3')) 6ffd3d3
Concatenated -c$code e22217a
Colon-bound launcher target (-FilePath:$p) 352dd78
Multi-option computed launcher (-NoNewWindow -FilePath $exe) 65e4c33

One Minor Observation (Non-blocking)

ps-command.sh:247:

if ps::has_launcher "$recovered" && [[ "$blanked" == *'$'* || "$blanked" == *'('* ]]; then

ps::has_launcher receives $recovered (already backtick-stripped) but internally strips backticks again (local lc="${1//\/}"`). The double-strip is harmless — just redundant. Not worth changing unless you're doing a separate cleanup pass.


Test Coverage — Complete

201 fixtures per commit message (PASS=201 FAIL=0). The new fixture at line 574 covers the exact evasion form that drove the change (-NoNewWindow -FilePath $exe with two named params before the computed target). The existing Start-Process notepad fixture at line 576 confirms the false-positive guard. All prior attack forms from the review history remain covered.


CHANGELOG and Version Bump

plugin.json correctly bumped to 0.14.2. The CHANGELOG entry is comprehensive — it names every evasion form covered, the deliberate divergence from the Bash lane, the accepted over-block behavior, and the acknowledged residuals (python3 $flag, py -3, stdin heredoc).


Verdict

Approved. The conservative any-construct check correctly closes the whole parameter-ordering/binding sub-class with a single structural rule instead of a regex that kept being outpaced. The false-positive guard (write-indicator tokens in quoted strings don't survive quote-blanking) is sound, and the implementation mirrors the established sink doctrine. All P1 security findings from the full 12-commit review history are closed at HEAD.

@claude

claude Bot commented Jul 24, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 6m 7s —— View job


Security Review

  • Get PR diff at HEAD (65e4c33)
  • Read changed files
  • Analyze latest commit vs. prior open findings
  • Post security findings

What 65e4c33 Changes

The final commit replaces the per-parameter launcher regex in ps::might_write_via_python3 with a conservative two-condition check:

blanked=$(ps::blank_quoted_spans "$1")
if ps::has_launcher "$recovered" && [[ "$blanked" == *'$'* || "$blanked" == *'('* ]]; then
  return 0
fi

Moving from: "did start-process appear followed by at most one named parameter then $/(?" to: "does any launcher keyword appear AND does the quote-blanked command carry any unquoted $ or (?" — strictly wider blocking that defeats the multi-option binding variants the prior regex missed.


Prior P1 from 352dd78CLOSED

Codex P1: $exe = 'py'+'thon3'; Start-Process -NoNewWindow -FilePath $exe -ArgumentList '-c','open(...)' bypassed the old per-parameter regex because two named parameters precede the computed -FilePath slot.

Trace through new code:

  1. COMMAND_LC =~ $_py_writeopen( present → TRUE
  2. ps::blank_herestrings "$COMMAND" → no here-string → PS_BLANKED = raw command
  3. ps::might_write_via_python3 "$PS_BLANKED":
    • Single-quote pass strips 'py', 'thon3', '-c', 'open("x","w").write("a")'$exe = +; Start-Process -NoNewWindow -FilePath $exe -ArgumentList ,
    • Double-quote pass: nothing to remove
    • blanked = $exe = +; Start-Process -NoNewWindow -FilePath $exe -ArgumentList , — contains $
    • ps::has_launcher("$recovered"): start-process after ; boundary → returns 0 (true) ✓
    • Both conditions true → returns 0 → BLOCKED

Logic and Injection Surfaces — No Issues

1. blanked is from ps::blank_quoted_spans "$1" (not $recovered). $1 is $PS_BLANKED (here-string blanked) — backticks are NOT stripped before blanked is computed. This is sound: the glob checks *'$'* and *'('* look for the literal $/( characters as bash glob patterns, and a backtick before $ or ( doesn't remove those characters from the string. A `$p sequence still contains $; `( still contains (.

2. No injection path on any new surface. ps::blank_quoted_spans "$1" feeds $1 via printf '%s' into sed — user-controlled content enters via stdin only, never the sed expression. ps::has_launcher "$recovered" uses $lc (derived from $recovered) on the LEFT side of [[ =~ ]] (quoted). The glob match [[ "$blanked" == *'$'* || ... ]] has $blanked quoted on the left — user content cannot affect the pattern.

3. ps::has_launcher receives here-string-blanked input. A launcher keyword inside a here-string body would be replaced with PS_HERESTRING_PLACEHOLDER before ps::has_launcher sees it — no false positive from a quoted launcher mention.

4. The false-positive guard holds. Start-Process notepad -ArgumentList '-c','open("x","w")':

  • After quote-blanking: single-quote pass removes '-c' and 'open("x","w")'Start-Process notepad -ArgumentList , — no $ or (
  • ps::has_launcher returns true (matches start-process), but second condition fails → NOT triggered → falls through to literal python3 check → python3 absent → ALLOWED

5. New code is strictly more restrictive than 352dd78. The old per-parameter regex admitted at most one named parameter before the computed slot. The new check is parameter-count-agnostic: any launcher + any unquoted $/( blocks. No prior-blocked case becomes newly-allowed.


Previously Resolved Findings — Confirmed Still Closed at HEAD

Finding Commit Status
Original PS lane gap (python3 -c via PS tool) 86d06cc
Here-string body (@'\npython3 -c open\n'@) 053425b (sink pivot)
<# … #> inline block-comment 053425b (sink doesn't parse comments; tokens still present)
Backtick obfuscation (python3 `-c) 053425b
Call-operator + quoted interpreter (& 'python3' -c) 053425b
Path-qualified interpreter (\python3.exe) 053425b
Invoked script block (&{python3 -c}) 053425b
-ArgumentList '-c',… arg-splitting 053425b
Computed -c via ('-'+'c') 3eeb06f
Computed launcher target ('py'+'thon3') 6ffd3d3
Concatenated -c$code e22217a
Colon-bound -FilePath:$p 352dd78
Multi-option computed target (-NoNewWindow -FilePath $exe) 65e4c33

No security vulnerabilities found. The conservative launcher check in 65e4c33 correctly closes the multi-option computed target class without introducing injection surfaces, logic gaps, or regressions in prior fixes. The fail-closed direction (over-block, never under-block) is maintained throughout.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 65e4c33909

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread plugins/guardrails/lib/powershell/ps-command.sh
…get (review P1)

A call `&` / dot-source `.` of a DOUBLE-quoted interpolated target still bypassed
the sink: `& "$env:PYTHON_BIN" -c "open('x','w')..."` resolves to python3, but
blank_quoted_spans erases the quoted target, it is not a launcher, and it carries
no literal python3 token — so ps::might_write_via_python3 returned false.
ps::write_bypass misses it too (its computed-target check wants an UNQUOTED
`& $`/`& (`).

ps::might_write_via_python3 now fails closed on a call/dot-source of a
double-quoted target that interpolates a variable or subexpression (matched on the
quote-intact text before the token test). A SINGLE-quoted target does not
interpolate in PowerShell (`& '$x'` is the literal name `$x`), so it is not matched
and stays allowed via write_bypass's arbitrary-quoted-program residual.

Two fixtures added (interpolated `& "$env:PYTHON_BIN"` MUST block; single-quoted
`& '$x'` MUST stay quiet). Suite PASS=203 FAIL=0. Folded into 0.14.2.

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

claude Bot commented Jul 24, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 4m 42s —— View job


Review

  • Gather context and read changed files at HEAD (9d6c979)
  • Analyze latest commit (interpolated call-operator python target fix)
  • Verify prior findings remain closed
  • Post findings

What 9d6c979 Addresses

The last Codex P1 — that & "$env:PYTHON_BIN" -c "open('x','w').write('a')" bypassed the sink because blank_quoted_spans erases the double-quoted target, it is not a launcher, and no literal python3 token remains for the token test — is correctly fixed.

The fix adds one new check to ps::might_write_via_python3 (ps-command.sh:258), positioned after the launcher check and before the literal-token test:

[[ "$lc" =~ (^|[[:space:]\;\{\}\(\|\&])[.\&][[:space:]]*\"[^\"]*\$ ]] && return 0

Mechanics Verified

& "$env:PYTHON_BIN" -c "open('x','w').write('a')" — blocked:

  • lc = & "$env:python_bin" -c "open('x','w').write('a')"
  • Boundary: ^& matches [.\&] → space matches [[:space:]]*" matches "[^"]* matches 0 chars, then \$ matches the $ of $env:MATCH → returns 0 → blocked

Path-qualified interpolated target & "C:\users\$env:USERNAME\python3.exe" — blocked:

  • After ": [^"]* matches C:\users\, then \$ matches $ of $env:MATCH

Single-quoted & '$x' -c "print open(" — allowed:

  • After & [[:space:]]*, the next char is ' (single quote), not "no match → falls through → no python3 token → allowed

Literal double-quoted non-python & "notepad" -c "open(...)" — allowed:

  • After "notepad": [^"]* exhausted at ", no $ found → no match → falls through → notepadpython3allowed

Backtick-obfuscated & "`$env:PYTHON_BIN" — blocked:

  • recovered = "${1//\/}"strips backtick → ``$env `` → $env → `$` present → MATCH

& "$(expr)" -c "..." (subexpression target) — blocked:

  • $ in $(expr)\$ matches → MATCH

Correct ordering. The new check appears before the python3 token test — which is the only valid position, since the double-quoted target is erased by blank_quoted_spans and the token test would therefore miss it.


[^\"] Character Class — Confirmed Correct

In bash ERE inside [[ =~ ]], backslash inside [...] is treated as a literal character — so [^\"] is effectively [^"] (non-double-quote chars). The \" is consistent with the codebase's existing style (e.g. in ps::blank_quoted_spans's sed scripts). No issue.


False-Positive Guard at the Call Site — Holds

The hook still gates on $COMMAND_LC =~ $_py_write first, so a non-write command like & "$env:PYTHON_BIN" --list carries no write indicator and never reaches ps::might_write_via_python3. The new check only fires when co-occurrence is established.


All Prior P1 Findings — Confirmed Closed at HEAD (9d6c979)

Finding Commit Status
Original PS lane gap (python3 -c via PS tool) 86d06cc
Here-string body (@'\npython3 -c open\n'@) 053425b (sink pivot)
<# … #> inline block-comment 053425b (sink; tokens still visible)
Backtick obfuscation / line-continuation 053425b
Call-operator + quoted interpreter (& 'python3') 053425b
Path-qualified interpreter (\python3.exe) 053425b
Invoked script block (&{python3 -c}) 053425b
-ArgumentList '-c',… arg-splitting 053425b
Computed -c via ('-'+'c') 3eeb06f
Computed launcher target (-FilePath ('py'+'thon3')) 6ffd3d3
Concatenated -c$code e22217a
Colon-bound -FilePath:$p 352dd78
Multi-option computed target (-NoNewWindow -FilePath $exe) 65e4c33
Interpolated double-quoted call-op (& "$env:PYTHON_BIN") 9d6c979

Accepted Residuals (Documented, Pre-Existing)

  • python3 $flag … where $flag = '-c' at runtime — variable command word; shared Bash+PS residual, same as the git lane
  • py -3, bare python, python3.11 — alternate interpreter spellings
  • python3 - <<PY … PY — stdin heredoc (no -c)
  • Multi-line <# … #> block comments (opener/closer on separate lines) — the single-line form is covered by the sink approach; multi-line is substantially rarer in LLM-generated tool calls

Optional Follow-Up (Non-Blocking, Carry-Over)

Double-quoted mention testWrite-Output "python3 -c open() later" as a must-stay-quiet fixture. The mechanism handles it correctly (the token+-c co-occurrence over-blocks it the same way as the single-quoted mention and line-comment forms, all of which are already documented at line 523). The test matrix has the single-quote form only. Flagged as optional since round 1; the behavior is documented and intentional.


Verdict

Approved. The interpolated call-operator fix is correctly implemented, positioned at the right point in the check sequence, tested with two well-chosen fixtures (MUST block and MUST stay quiet), and introduces no new injection surfaces. The 13-commit review series has converged: ps::might_write_via_python3 now covers the complete class of PowerShell token obfuscation and computed-invocation forms, and every P1 security finding surfaced across all review rounds is closed at HEAD.

@claude

claude Bot commented Jul 24, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 9m 33s —— View job


Security Review

  • Get PR diff
  • Read changed files at HEAD (9d6c979)
  • Analyze ps::might_write_via_python3 for injection surfaces and bypass vectors
  • Verify interpolated call-operator target fix (last open Codex P1)
  • Check function ordering, scoping, and existing-fix regressions
  • Post security findings

No security vulnerabilities found in this PR's changes. Detail follows.


What 9d6c979 changes

The final commit adds one check to ps::might_write_via_python3 (ps-command.sh:258), inserted between the launcher-computed-target check (line 247) and the literal python3 token test (line 260):

[[ "$lc" =~ (^|[[:space:]\;\{\}\(\|\&])[.\&][[:space:]]*\"[^\"]*\$ ]] && return 0

Codex P1 closed: & "$env:PYTHON_BIN" -c "open('x','w').write('a')" previously bypassed the sink because ps::blank_quoted_spans erases the double-quoted target before the token scan, it is not a launcher, and it carries no literal python3 token. The new check fires on the quote-INTACT $lc before those tests run.


Regex mechanics — confirmed correct

\$ matches a literal dollar sign, not the end-of-string anchor. In bash [[ =~ ]], the compound command does not strip backslashes from the ERE pattern the way unquoted word expansion does in other contexts — the backslash is forwarded to the ERE engine. The ERE engine receives \$, which it treats as an escaped (literal) $. The effective regex is:

(^|[[:space:]\;\{\}\(\|\&])[.\&][[:space:]]*"[^"]*$LITERAL

Trace for & "$env:python_bin" -c "open('x','w').write('a')" (the target):

  1. ^ boundary, & matches [.\&], matches [[:space:]]*, " matches the opening double-quote.
  2. [^"]* matches zero characters (greedy but the literal \$ immediately follows).
  3. Literal \$ matches $ at the start of $env:python_bin.
  4. MATCH → return 0 → blocked ✓

Trace for & '$x' -c "print open(" (the must-stay-quiet fixture):

The & is followed by ' (single-quote), not ". No position in the string satisfies [.\&][[:space:]]*" with a $ inside the double-quoted span. No match → falls through to token test → python3 absent → return 1 → allowed ✓

[.\&] character class — correct. Inside [...], . is literal (not the "any char" metachar) and \& is &. Class = {., &}, correctly covering both the call operator and dot-source operator.


Injection surfaces — none introduced

$lc in [[ "$lc" =~ … ]]: $lc is derived from ${1//\/}(backtick strip, pure bash parameter expansion) then lowercased. It appears on the **left-hand side** of=~`, where it is quoted — user-controlled content cannot affect the regex pattern. No injection path.

Pattern is a script literal — no $VAR expansion inside the regex itself. The \$ is a literal pattern element; it is not expanded from any user-supplied value.


Function ordering — correct

The new interpolated-target check is correctly placed:

Order Check Catches
1 Launcher + computed construct (line 247) Start-Process … $exe …
2 [NEW] Interpolated call-op target (line 258) & "$var" -c …
3 Literal python3 token (line 260) & "python3" …, /path/python3
4 Literal -c flag (line 263) python3 -c "open(…)"
5 Concatenated -c$code (line 269) python3 -c$code
6 Special-construct fallback (line 275) python3 ('-'+'c') …

The new check MUST run before line 260, because ps::blank_quoted_spans (called at line 235 to compute blanked) would have erased "$env:PYTHON_BIN" — making the token test at line 260 unable to see a python3 token. Inserting it at line 258 is correct.


Variable scoping — clean

recovered, lc, q, blanked are all local to ps::might_write_via_python3. PS_BLANKED is set by ps::blank_herestrings "$COMMAND" before the call and consumed before the PowerShell block's exit 0. No bleed into the Bash lane below. ✓


All prior P1 findings — confirmed closed at HEAD (9d6c979)

Finding Commit Status
Original PS lane gap (python3 -c via PS tool) 86d06cc
Here-string body (@'\npython3 -c open\n'@) 053425b (sink pivot)
<# … #> block-comment bypass 053425b (sink never parses comments)
Backtick obfuscation / line-continuation 053425b (${1//\/}` strip)
Call-operator + quoted interpreter (& 'python3' -c) 053425b (quote-intact probe)
Path-qualified interpreter (\python3.exe) 053425b (token boundary admits .)
Invoked script block (&{python3 -c}) 053425b (quote-intact probe)
-ArgumentList '-c',… arg-splitting 053425b (literal -c quote-bounded)
Computed -c via ('-'+'c') 3eeb06f (special-construct fallback)
Computed launcher target (-FilePath ('py'+'thon3')) 6ffd3d3 (launcher clause)
Concatenated -c$code e22217a (-c[\$\(] pattern)
Colon-bound computed launcher (-FilePath:$p) 352dd78 (([[:space:]]+|:) alt)
Multi-option computed launcher (-NoNewWindow -FilePath $exe) 65e4c33 (conservative blanked-scan)
Interpolated call-op target (& "$env:PYTHON_BIN" -c) 9d6c979

Verdict: no security findings. The interpolated call-operator fix is minimal, correctly targeted, and introduces no new injection surfaces or logic gaps. The full 13-commit review history has converged on a complete, fail-closed sink implementation.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9d6c9796cf

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread plugins/guardrails/lib/powershell/ps-command.sh
@kyle-sexton

Copy link
Copy Markdown
Contributor Author

CI classification (pre-merge)

All required checks passed on head 9d6c9796 — no infra-fail, no fail-open path taken:

  • ci (hygiene, plugin-gate, all sync/drift/skill gates, portability-lint, changelog-parity, zizmor) — pass
  • claude-review (review / review) — pass
  • claude-security-review (security-review / changes, security-review / security-review) — pass
  • do-not-merge, pr-issue-linkage, pr-title, GitGuardian — pass

Local guard suite: PASS=203 FAIL=0 (shellcheck clean).

Review-thread disposition

9 Codex P1 rounds on the PowerShell python-write sink. Rounds 1–8 were genuine gaps and are fixed (fold into 0.14.2): the fail-closed pivot to the sink doctrine, plus computed--c, computed/colon/multi-option launcher targets, and interpolated call-operator targets. Round 9 (py"thon3", quote-split command name) was resolved as a documented shared Bash+PowerShell residual — verified the Bash lane allows the identical input (exit 0), so per the guard's own scope (ps-command.sh header: "THE BAR IS BASH-PARITY, NOT AIRTIGHT … a command word supplied entirely by an unexpanded variable … shared Bash+PS residuals, documented, not bugs") command-name obfuscation is out of scope for both lanes. Rationale posted inline on that thread.

Merging per the standing directive (drive to MERGED with a CI-classification comment; claude-review / claude-security-review are fail-open on infra-fail — not exercised here, all green).

@kyle-sexton
kyle-sexton merged commit ebd5a31 into main Jul 24, 2026
25 checks passed
@kyle-sexton
kyle-sexton deleted the fix/guardrails-ps-interpreter-write-bypass branch July 24, 2026 04:59
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.

guardrails: block-hook-bypass PowerShell lane skips the interpreter-producer scan (python3 -c write bypass)

1 participant