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
13 changes: 13 additions & 0 deletions hooks/no-chaining.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,19 @@ command=$(jq -r '.tool_input.command // empty')
# Strip single-quoted and double-quoted strings to avoid false positives
stripped=$(printf '%s' "$command" | sed "s/'[^']*'//g" | sed 's/"[^"]*"//g')

# Strip heredoc content — the body of a <<'WORD' block is literal text, not
# shell syntax. Any |, &&, or || inside it are not operators. Keep only the
# first line (the invocation line with the heredoc opener) for chaining
# analysis, since that's the only line that can contain real shell operators.
#
# Known limitation: a real pipe placed after the heredoc closer on a subsequent
# line (e.g. <<'EOF' ... EOF ) | bash) will not be detected. This is an
# accepted trade-off given the rarity of that pattern and the existing
# known limitation around $() subshells.
if printf '%s' "$stripped" | grep -qF '<<'; then
stripped=$(printf '%s' "$stripped" | head -n 1)
fi

# Check if a 'git' segment uses only safe read-only subcommands.
# Handles optional flags before the subcommand (e.g. git -C /path log ...).
# Each flag group may optionally consume one value token (e.g. -C /path).
Expand Down
21 changes: 21 additions & 0 deletions hooks/no-chaining.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,27 @@ assert_blocked "blocks: npm test piped to xargs" \
assert_blocked "blocks: npm test piped to bash" \
"npm test | bash"

# ========== Heredoc false positives (issue #21) ==========
#
# | characters inside a heredoc body are literal text, not pipe operators.
# The hook must not block commands whose only pipes are in heredoc content.

assert_allowed "allows: gh issue edit with | in heredoc body (option list)" \
$'gh issue edit 123 --repo owner/repo --body "$(cat <<\'BODY\'\n## Inputs\n\n- `sensitivity` — optional: `public | internal | confidential | restricted`\n\nBODY\n)"'

assert_allowed "allows: gh issue create with | in heredoc body (markdown table)" \
$'gh issue create --repo owner/repo --title "KG explorer" --body "$(cat <<\'EOF\'\n| Field | Display |\n|---|---|\n| Label | Large heading |\nEOF\n)"'

assert_allowed "allows: gh pr create with | in heredoc body (type union)" \
$'gh pr create --title "feat: add types" --body "$(cat <<\'EOF\'\nAccepts `string | number | null`.\nEOF\n)"'

# A real pipe on the heredoc opener line (not in the body) must still be caught.
assert_blocked "blocks: real unsafe pipe on heredoc opener line" \
$'cat <<\'EOF\' | bash\nsome content\nEOF'

assert_allowed "allows: safe pipe on heredoc opener line" \
$'cat <<\'EOF\' | grep foo\nsome content\nEOF'

# ========== Summary ==========

printf '\n%d passed, %d failed\n' "$passed" "$failed"
Expand Down
Loading