Files
2026-08-04 10:45:25 +02:00

48 lines
1.7 KiB
Bash
Executable File

#!/usr/bin/env bash
# PreToolUse hook for the Bash tool. Does NOT silently block anything —
# per design, it only ever forces the normal confirmation prompt
# (permissionDecision: "ask") when a Bash command contains shell chaining
# operators. It never returns "deny" and never returns "allow" itself; it
# either stays out of the way (no JSON output) or asks. This exists as a
# safety net against Claude Code's Bash allow-list not always splitting
# compound commands the way the docs describe (see e.g.
# https://github.com/anthropics/claude-code/issues/20085), so a chained
# command can't slip through on a whitelisted prefix without the user
# seeing it.
#
# Register in settings.json under hooks.PreToolUse with a matcher of "Bash".
set -euo pipefail
INPUT="$(cat)"
if ! command -v jq >/dev/null 2>&1; then
# No jq available — fail open (no output = no opinion), rather than
# breaking every Bash call because a dependency is missing.
exit 0
fi
TOOL_NAME="$(echo "$INPUT" | jq -r '.tool_name // empty')"
if [ "$TOOL_NAME" != "Bash" ]; then
exit 0
fi
CMD="$(echo "$INPUT" | jq -r '.tool_input.command // empty')"
# Look for shell chaining/substitution operators anywhere in the command.
# Deliberately broad (better a false positive prompt than a missed chain):
# && || ; | backticks $( )
if echo "$CMD" | grep -qE '[;&|`]|\$\('; then
jq -n \
--arg reason "Command appears to chain multiple shell commands (&&, ;, |, or command substitution). Forcing a normal confirmation prompt instead of relying on the allow-list, since compound commands can bypass per-command whitelisting." \
'{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "ask",
permissionDecisionReason: $reason
}
}'
fi
exit 0