Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "guardrails",
"version": "0.3.1",
"version": "0.3.2",
"description": "Six safety guards that block secret/credential writes, hardcoded machine-specific paths, git hook-bypass attempts, Bash file-write workarounds that circumvent Write/Edit hooks, (advisory) hallucinated CLI flags, and (advisory) un-throttled Workflow fan-out that risks burst 529s — each independently toggleable.",
"author": {
"name": "Melodic Software",
Expand Down
15 changes: 11 additions & 4 deletions plugins/guardrails/hooks/block-hook-bypass.sh
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,12 @@ strip_literals() {
local cmd="$1" line result="" in_heredoc=0 delim="" trimmed
# `(^|[^<])` before `<<` excludes a here-string `<<<` — matching `<<` inside
# `<<<` would capture a bogus delimiter and strand the stripper in-heredoc,
# swallowing every later line (a here-string bypass). The delimiter's first
# char is `[^[:space:]<]` for the same reason.
local heredoc_start_re='(^|[^<])<<-?[[:space:]]*([^[:space:]<][^[:space:]]*)'
# swallowing every later line (a here-string bypass). The delimiter body
# excludes `<` for the same reason, and `>` so a redirect glued to the
# delimiter (`cat <<EOF>file`) terminates the token — bash ends the delimiter
# word at `>`, so the `>file` is a real redirect that must reach the scan
# instead of being swallowed into a bogus `EOF>file` delimiter.
local heredoc_start_re='(^|[^<])<<-?[[:space:]]*([^[:space:]<>]+)'

while IFS= read -r line || [[ -n "$line" ]]; do
if ((in_heredoc)); then
Expand All @@ -109,7 +112,11 @@ strip_literals() {
delim="${delim%\'}"
delim="${delim#\"}"
delim="${delim%\"}"
line="${line%%<<*}"
# Drop only the heredoc operator + delimiter token, keeping the text before
# `<<` AND any text after the delimiter on the opener line — a trailing
# stdout redirect (`cat <<EOF > file`) must still reach the redirect scan
# rather than being truncated away with the body.
line="${line%%<<*}${line#*"${BASH_REMATCH[0]}"}"
in_heredoc=1
fi
line=$(printf '%s' "$line" | sed "s/'[^']*'//g" | sed -E 's/"([^"\\]|\\.)*"//g')
Expand Down
22 changes: 22 additions & 0 deletions plugins/guardrails/hooks/block-hook-bypass.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,28 @@ run "heredoc metachar delim, trailing cat > bypass (blocked)" "$HEREDOC_METACHAR
HEREDOC_BACKSLASH=$(printf 'cat <<\\EOF\ncontent line\nEOF\ncat > real.txt')
run "heredoc backslash delim, trailing cat > bypass (blocked)" "$HEREDOC_BACKSLASH" 2

# A stdout redirect ON the heredoc opener line (`cat <<EOF > file`) is a real
# file-write bypass. The strip must drop only the heredoc operator + delimiter,
# keeping the trailing `> file` so the redirect scan still fires — truncating
# everything after `<<` would leak this form (exit 0).
HEREDOC_OPENER_REDIR=$(printf 'cat <<EOF > real.txt\ncontent line\nEOF')
run "heredoc opener stdout redirect (blocked)" "$HEREDOC_OPENER_REDIR" 2
# A plain heredoc with NO redirect on the opener stays allowed.
HEREDOC_NO_REDIR=$(printf 'cat <<EOF | cat\ncontent line\nEOF')
run "heredoc opener, no redirect (allowed)" "$HEREDOC_NO_REDIR" 0
# Redirect GLUED to the delimiter (no space): bash ends the delimiter word at
# `>`, so `cat <<EOF>real.txt` is a real stdout redirect. The delimiter capture
# must stop at `>` (not greedily swallow `EOF>real.txt`) so the `>` survives.
HEREDOC_GLUED_REDIR=$(printf 'cat <<EOF>real.txt\ncontent line\nEOF')
run "heredoc opener redirect glued to delimiter (blocked)" "$HEREDOC_GLUED_REDIR" 2
# Tab-stripping opener form (`<<-EOF`): the `<<-?` regex + fix cover it.
HEREDOC_TAB_STRIP=$(printf 'cat <<-EOF > real.txt\ncontent line\nEOF')
run "heredoc tab-strip opener redirect (blocked)" "$HEREDOC_TAB_STRIP" 2
# Quoted delimiter (`<<'EOF'`): BASH_REMATCH[0] is `<<'EOF'`; the suffix scan
# still preserves the trailing redirect.
HEREDOC_QUOTED_DELIM=$(printf "cat <<'EOF' > real.txt\ncontent line\nEOF")
run "heredoc quoted-delimiter opener redirect (blocked)" "$HEREDOC_QUOTED_DELIM" 2

# A here-string (<<<) has no terminator — it must NOT be mistaken for a heredoc,
# which would strand the stripper and swallow the trailing bypass.
HERESTRING=$(printf 'read x <<< %sok%s\ncat > real.txt' '"' '"')
Expand Down
Loading