Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
952befe
perf(guardrails): run the guard chain in-process and answer payload f…
kyle-sexton Sep 15, 2026
e4cb218
perf(guardrails): make ps-command.sh fork-free
kyle-sexton Sep 15, 2026
d809a9e
fix(guardrails): exempt a quoted git literal in comparison position
kyle-sexton Sep 15, 2026
b77feaf
fix(guardrails): treat item and job launchers as executors in the Pow…
kyle-sexton Sep 15, 2026
241e256
fix(guardrails): exempt a compared git literal only inside a read-onl…
kyle-sexton Sep 15, 2026
df80eb8
fix(guardrails): predeclare the nameref-assigned SUBJECT for shellcheck
kyle-sexton Sep 15, 2026
9ad209f
Merge branch 'guardrails-run-guards-in-process-no-subs' into guardrai…
kyle-sexton Sep 15, 2026
04fc869
test(lib): use a portable cwd in the fast-fields payload fixture
kyle-sexton Sep 16, 2026
c83d1c8
fix(guardrails): refuse the compared-literal exemption on any expanda…
kyle-sexton Sep 16, 2026
502416c
Merge branch 'guardrails-run-guards-in-process-no-subs' into guardrai…
kyle-sexton Sep 16, 2026
551629e
test(lib): keep the fixture cwd free of a \w escape for the portabili…
kyle-sexton Sep 16, 2026
919bf66
docs(guardrails): drop the em dashes from the 0.35.0 entry
kyle-sexton Sep 16, 2026
b0377b9
chore(guardrails): satisfy the machine-path and portability lint lanes
kyle-sexton Sep 16, 2026
702c629
test(guardrails): use a portable Windows path in the read-only pipeli…
kyle-sexton Sep 16, 2026
6e16c10
fix(hook-utils): gate the builtin field parser on Bash 4.0 and size i…
kyle-sexton Sep 16, 2026
915423a
docs(guardrails): name run-guards.sh as the chain slot's one exception
kyle-sexton Sep 16, 2026
ab2a9b1
chore: merge main into the in-process guards branch and rebump the co…
kyle-sexton Sep 16, 2026
a158925
refactor(guardrails): prefix the locals of the out-parameter helpers
kyle-sexton Sep 16, 2026
7f1da17
Merge remote-tracking branch 'origin/guardrails-run-guards-in-process…
kyle-sexton Sep 16, 2026
8527653
merge: origin/main into guardrails-powershell-guard-path-3x-slow
kyle-sexton Sep 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plugins/guardrails/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -147,5 +147,5 @@
"min": 1
}
},
"version": "0.34.0"
"version": "0.35.0"
}
7 changes: 7 additions & 0 deletions plugins/guardrails/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
All notable changes to the `guardrails` plugin are documented here. Format follows
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning.

## [0.35.0]

### Changed

- lib/powershell/ps-command.sh spawns nothing. Every `$(ps::…)` capture is now an out-parameter helper that assigns with `printf -v` (`ps::blank_quoted_spans_to`, `ps::opaque_quoted_spans_to`, `ps::call_site_operand_region_to`, `ps::blank_bracket_interiors_to`, `ps::fold_escaped_brace_closers_to` and the three `ps::_skip_*_to` index walkers, the `_to` convention hook-utils.sh already uses), and every `printf | sed` pipeline and `< <(printf …)` line reader is a pure-bash substitution or split (`ps::_gsub_to`, which applies an ERE per line exactly as sed hands its regex one line at a time, and `ps::_split_lines_to`). On Windows Git Bash the PowerShell lane's PreToolUse chain went from 80 process creations to 3, the harness's `bash -c`, the `env` of the shebang, and bash itself, and from 2422 ms to 300 ms isolated p50; a PowerShell command that carries a script block and a git token, so it reaches the fail-closed sink, from 44 creations and 1433 ms to 3 and 299 ms. The Bash lane, which never loads this library, stays at 3 creations and 288 ms to 274 ms. Every deny and allow is byte-identical (rc, stdout and stderr) over 34 Bash and PowerShell invocations of the perf baseline's 17-command corpus and over 653 commands harvested from the guard suites on each lane. Two host behaviors the captured forms carried are reproduced rather than dropped, because a PowerShell command arrives with Windows line endings and PS_SAFE_COMMAND goes on to a Bash tokenizer: `$(…)` here eats a trailing CRLF whole, not just its LF, and this host's `sed` reads in text mode, so a CRLF line ending loses its CR.
- A quoted `git` string literal in COMPARISON-OPERAND position no longer engages the PowerShell fail-closed sink. `Get-Process | Where-Object { $_.Name -eq 'git' }`, `… -ceq 'git'`, `… -in @('git.exe','bash.exe')`, `… -notin @('git','node')` and `… -like 'git*'` are read-only pipelines that merely NAME git, and the script block alone routed them to "cannot be parsed with confidence and could reach git"; they are now allowed. The literal is blanked before the git probe only when BOTH hold: its nearest preceding non-whitespace token is a PowerShell comparison operator (`-eq -ne -in -notin -contains -notcontains -like -notlike -match -notmatch -lt -le -gt -ge`, with an optional `c`/`i` case prefix, reached through an optional `(` / `@(` and through `,`-separated earlier elements of the same list; RIGHT-hand operands only, because `& 'git' -eq $x` is a call, not a comparison), AND the whole command is provably a READ-ONLY CMDLET PIPELINE. That second half is an ALLOWLIST, not a list of known executors: with every quoted string replaced by an opaque placeholder, the command is refused outright when it carries ANY EXPANDABLE STRING anywhere (a double-quoted `"…"` span, or a `@"…"@` here-string), on a surviving backtick, `<#`, `--%`, `::`, `[`, `&` or a `.` before `(`, and is then walked token by token so that every token standing at a command position (the start of input, or after `|`, `;`, `{`, `}`, `(`, `=` or a newline) is an allowlisted interrogator: any `Get-*` verb, `gps`/`ps`/`gcim`/`gwmi`/`gci`/`ls`/`dir`/`gi`/`gc`/`cat`/`type`/`gsv`/`gcm`/`gmo`/`gv`/`gl`/`pwd`, `Where-Object`/`?`, `Select-Object`, `ForEach-Object`/`%`, `Sort-Object`, `Measure-Object`, `Group-Object`, the `Format-*` and `Out-*` set, `Write-Output`/`Write-Host`/`echo`, `Select-String`/`sls`, the path helpers, `Compare-Object`/`diff`, the `ConvertTo-*`/`ConvertFrom-*` pair, and the `if`/`else`/`elseif`/`in`/`return` keywords. `$variable` chains, `-parameter` tokens, string placeholders, numbers and ARGUMENTS (`Get-CimInstance Win32_Process`, `Select-Object ProcessId`) are inert; any other command word refuses. The expandable-string disqualifier is there because such a string is EVALUATED where it is written: `"$( … )"` runs its subexpression to build the value, so the operand itself is a command position, and the very placeholder that keeps the scan honest about message text is what hides it. A fresh security review reproduced the whole executor family through that one hole, under `-eq`, `-ceq`, `-like`, `-in`, inside a second `Where-Object`, behind `Get-Content`, and interpolated beside `${env:ComSpec}`: `"$(cmd /c git push --force)"`, `"$(bash -c 'git push --force')"`, `"$(powershell -c 'git reset --hard')"`, `"$(Start-Process git -ArgumentList push,--force)"`, `"$(& 'git' push -f)"`, `"$(Start-Job { git push -f })"`, `"$(Invoke-Item git.exe)"`, `"$(New-Object …)"`, `"$(node -e 'x')"`, `"$(schtasks /create … /tr 'git push -f')"` and a bare `"$(git push --force)"`. The refusal is on the `"` itself, so it does not depend on recognizing any of them. A `@"…"@` here-string is blanked out of the command at intake, taking its git token with it, so that form is refused at the fail-closed sink instead, where the blanking happened; a verbatim `@'…'@` body carries no command position and is untouched. VERBATIM `'…'` operands, which is every shape this bullet exempts, are unaffected. Inverting the test is what makes it sound: a blocklist of executors is structurally under-inclusive, and an unrecognized command word now costs an over-block instead of a bypass. Everything else keeps the quote-intact probe and stays blocked: `& 'git' commit --no-verify`, `git 'commit'`, `Start-Process 'git' reset --hard`, `saps 'git' -ArgumentList 'push -f'`, `cmd /c 'git push --force'`, `$n = 'git'; & $n push -f`, `'git' | % { & $_ push -f }`, `'git' -in $names` (left-hand operand), an operand list long enough to exhaust the bounded walk, a command past the scan's length ceiling, and every `… -eq 'git' … | % { <exec> $_.Name }` form, that is `&`, `.`, `iex`, `Invoke-Command`, `cmd /c`, `bash -c`, `npx`, `dotnet`, `cscript`, `explorer`, `ssh`, `wmic process call create`, `schtasks /tr`, `Invoke-Item`/`ii`, `Start-Job`, `New-Object`, `New-Service`/`nsv`, `New-ScheduledTaskAction`, `iwmi -Class Win32_Process -Name Create`, `Set-Alias zz $_.Name; zz push --force`, `[Diagnostics.Process]::Start($_.Name, …)`, `[scriptblock]::Create($_.Name).Invoke()` and `$ExecutionContext.InvokeCommand.InvokeScript($_.Name)`, none of them named by the rule and all of them refused by it. Bounded by a two-root differential against the pre-narrowing tree: the 653-command corpus harvested from the guard suites is byte-identical on the PowerShell lane (0 decision changes, and 0 predicted: no command in it carries a git literal in operand position, and none carries a double-quoted string together with a compared git literal) and on the Bash lane, which never consults this function, and the perf baseline's 17-command corpus is identical across both tool modes. The designed corpus of exempt, counterexample and executor shapes, 47 commands, flips exactly 5, every one BLOCKED→ALLOWED with the sink message gone, allows 6 on both roots and leaves the other 36 blocked byte-for-byte. Extended with the 18 reviewed expandable shapes, 65 commands, it flips 6: those same 5, plus the `@"…"@` here-string operand ALLOWED→BLOCKED, with 6 allowed on both roots and 53 blocked byte-for-byte.

## [0.34.0]

### Changed
Expand Down
Loading
Loading