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
21 changes: 21 additions & 0 deletions .auto-claude-security.json.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"_comment": "ClawPinch command allowlist — controls which commands auto-fix can execute.",
"_doc": "Copy this file to one of the trusted locations below and customize.",
"_locations": [
"$CLAWPINCH_SECURITY_CONFIG (env var, highest priority)",
"<clawpinch-install-dir>/.auto-claude-security.json",
"~/.config/clawpinch/.auto-claude-security.json",
"~/.auto-claude-security.json"
],
"_security_note": "NEVER place this file inside a project being scanned — an attacker could override your allowlist.",

"base_commands": [
"echo", "jq", "grep", "cat", "ls", "pwd", "find", "sed", "awk", "wc",
"mkdir", "cd", "cp", "mv", "rm", "chmod"
],
"script_commands": [
"./clawpinch.sh"
],
"stack_commands": [],
"custom_commands": []
}
51 changes: 47 additions & 4 deletions clawpinch.sh
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,34 @@ export CLAWPINCH_SHOW_FIX="$SHOW_FIX"
export CLAWPINCH_CONFIG_DIR="$CONFIG_DIR"
export QUIET

# ─── Validate security config (early check for --remediate) ──────────────────
# Fail fast with a clear setup message instead of per-command failures later.

if [[ "$REMEDIATE" -eq 1 ]]; then
_sec_config_found=0

if [[ -n "${CLAWPINCH_SECURITY_CONFIG:-}" ]] && [[ -f "$CLAWPINCH_SECURITY_CONFIG" ]]; then
_sec_config_found=1
elif [[ -f "$CLAWPINCH_DIR/.auto-claude-security.json" ]]; then
_sec_config_found=1
elif [[ -f "$HOME/.config/clawpinch/.auto-claude-security.json" ]]; then
_sec_config_found=1
elif [[ -f "$HOME/.auto-claude-security.json" ]]; then
_sec_config_found=1
fi

if [[ "$_sec_config_found" -eq 0 ]]; then
log_error "Security config (.auto-claude-security.json) not found."
log_error "The --remediate flag requires a command allowlist to validate auto-fix commands."
log_error ""
log_error "Setup: copy the example config to a trusted location:"
log_error " cp .auto-claude-security.json.example ~/.config/clawpinch/.auto-claude-security.json"
log_error ""
log_error "Or set CLAWPINCH_SECURITY_CONFIG to point to your config file."
exit 2
fi
fi

# ─── Detect OS ───────────────────────────────────────────────────────────────

CLAWPINCH_OS="$(detect_os)"
Expand Down Expand Up @@ -295,10 +323,25 @@ else
_non_ok_count="$(echo "$_non_ok_findings" | jq 'length')"

if (( _non_ok_count > 0 )); then
log_info "Piping $_non_ok_count findings to Claude for remediation..."
echo "$_non_ok_findings" | "$_claude_bin" -p \
--allowedTools "Bash,Read,Write,Edit,Glob,Grep" \
"You are a security remediation agent. You have been given ClawPinch security scan findings as JSON. For each finding: 1) Read the evidence to understand the issue 2) Apply the auto_fix command if available, otherwise implement the remediation manually 3) Verify the fix. Work through findings in order (critical first). Be precise and minimal in your changes."
# Pre-validate auto_fix commands: strip any that fail the allowlist
# so the AI agent only receives pre-approved commands
_validated_findings_arr=()
while IFS= read -r _finding; do
_fix_cmd="$(echo "$_finding" | jq -r '.auto_fix // ""')"
if [[ -n "$_fix_cmd" ]] && ! validate_command "$_fix_cmd"; then
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If .auto-claude-security.json is missing, validate_command() will fail with error messages to stderr, but the script continues silently stripping all auto_fix commands. Users won't understand why auto-fix was removed from findings unless they notice the log_warn on line 306. Consider checking for config file existence before the loop and showing a clear setup message.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: clawpinch.sh
Line: 303:303

Comment:
If `.auto-claude-security.json` is missing, `validate_command()` will fail with error messages to stderr, but the script continues silently stripping all auto_fix commands. Users won't understand why auto-fix was removed from findings unless they notice the log_warn on line 306. Consider checking for config file existence before the loop and showing a clear setup message.

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

# Strip the disallowed auto_fix, keep finding for manual review
_finding="$(echo "$_finding" | jq -c '.auto_fix = "" | .remediation = (.remediation + " [auto_fix removed: command not in allowlist]")')"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validation errors suppressed. The validate_command call uses 2>/dev/null to silence stderr, which hides the root cause when validation fails (missing config file, python3 not found, JSON parse errors). Users will only see the warning on line 306 that auto_fix was stripped, but won't understand why.

Remove the stderr redirect so users can see validation failure reasons, or capture the error message and include it in the warning.

Prompt To Fix With AI
This is a comment left during a code review.
Path: clawpinch.sh
Line: 305:305

Comment:
Validation errors suppressed. The `validate_command` call uses `2>/dev/null` to silence stderr, which hides the root cause when validation fails (missing config file, python3 not found, JSON parse errors). Users will only see the warning on line 306 that auto_fix was stripped, but won't understand why.

Remove the stderr redirect so users can see validation failure reasons, or capture the error message and include it in the warning.

How can I resolve this? If you propose a fix, please make it concise.

log_warn "Stripped disallowed auto_fix from finding $(echo "$_finding" | jq -r '.id')"
fi
_validated_findings_arr+=("$_finding")
done < <(echo "$_non_ok_findings" | jq -c '.[]')
_validated_findings="$(printf '%s\n' "${_validated_findings_arr[@]}" | jq -s '.')"

_validated_count="$(echo "$_validated_findings" | jq 'length')"
log_info "Piping $_validated_count findings to Claude for remediation..."
echo "$_validated_findings" | "$_claude_bin" -p \
--allowedTools "Read,Write,Edit,Glob,Grep" \
"You are a security remediation agent. You have been given ClawPinch security scan findings as JSON. For each finding: 1) Read the evidence to understand the issue 2) If an auto_fix field is present, it contains a pre-validated shell command — DO NOT execute it directly. Instead, translate its intent into equivalent Read/Write/Edit operations. For example: a 'sed -i s/old/new/ file' becomes an Edit tool call; a 'jq .key=val file.json > tmp && mv tmp file.json' becomes Read + Write; a 'chmod 600 file' should be noted for manual action. 3) If no auto_fix, implement the remediation manually using Write/Edit 4) Verify the fix by reading the file. Work through findings in order (critical first). Be precise and minimal in your changes. IMPORTANT: You do NOT have access to Bash. Use only Read, Write, Edit, Glob, and Grep tools."
else
log_info "No actionable findings for remediation."
fi
Expand Down
183 changes: 183 additions & 0 deletions scripts/helpers/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,189 @@ require_cmd() {
fi
}

# ─── Command validation (allowlist) ─────────────────────────────────────────

validate_command() {
# Usage: validate_command <command_string>
# Returns 0 if ALL commands in the string are in allowlist, 1 otherwise
local cmd_string="$1"

if [[ -z "$cmd_string" ]]; then
log_error "validate_command: empty command string"
return 1
fi

# Find security config file (trusted locations only)
# SECURITY: Do NOT search the project being scanned — an attacker could
# include a malicious .auto-claude-security.json in their repo to override
# the allowlist and bypass all command validation.
local security_file=""

# 1. Explicit env var override (highest priority)
if [[ -n "${CLAWPINCH_SECURITY_CONFIG:-}" ]] && [[ -f "$CLAWPINCH_SECURITY_CONFIG" ]]; then
security_file="$CLAWPINCH_SECURITY_CONFIG"
fi

# 2. ClawPinch installation directory (next to clawpinch.sh)
if [[ -z "$security_file" ]]; then
local install_dir
install_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
if [[ -f "$install_dir/.auto-claude-security.json" ]]; then
security_file="$install_dir/.auto-claude-security.json"
fi
fi

# 3. User config directory (~/.config/clawpinch/)
Comment on lines +201 to +220
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Config file location search is better but not fully secure. The current implementation searches: (1) $CLAWPINCH_SECURITY_CONFIG, (2) <install-dir>/.auto-claude-security.json, (3) ~/.config/clawpinch/.auto-claude-security.json, (4) ~/.auto-claude-security.json.

The install directory lookup on line 208-209 is potentially vulnerable: if an attacker can manipulate ${BASH_SOURCE[0]} or if the script is symlinked from a compromised location, they could control which config is loaded.

Consider adding validation that the resolved $security_file path is owned by the current user and not world-writable before trusting it.

Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/helpers/common.sh
Line: 201:220

Comment:
Config file location search is better but not fully secure. The current implementation searches: (1) `$CLAWPINCH_SECURITY_CONFIG`, (2) `<install-dir>/.auto-claude-security.json`, (3) `~/.config/clawpinch/.auto-claude-security.json`, (4) `~/.auto-claude-security.json`. 

The install directory lookup on line 208-209 is potentially vulnerable: if an attacker can manipulate `${BASH_SOURCE[0]}` or if the script is symlinked from a compromised location, they could control which config is loaded. 

Consider adding validation that the resolved `$security_file` path is owned by the current user and not world-writable before trusting it.

How can I resolve this? If you propose a fix, please make it concise.

if [[ -z "$security_file" ]]; then
if [[ -f "$HOME/.config/clawpinch/.auto-claude-security.json" ]]; then
security_file="$HOME/.config/clawpinch/.auto-claude-security.json"
fi
fi

# 4. Home directory fallback
if [[ -z "$security_file" ]]; then
if [[ -f "$HOME/.auto-claude-security.json" ]]; then
security_file="$HOME/.auto-claude-security.json"
fi
fi

if [[ -z "$security_file" ]]; then
log_error "validate_command: .auto-claude-security.json not found. Searched: \$CLAWPINCH_SECURITY_CONFIG, <install-dir>/, ~/.config/clawpinch/, ~/. See .auto-claude-security.json.example for setup."
return 1
fi
Comment on lines +228 to +237
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing configuration file .auto-claude-security.json breaks validation. File is gitignored but required for validate_command() to work.

Suggested change
if [[ -z "$security_file" ]]; then
log_error "validate_command: .auto-claude-security.json not found"
return 1
fi
if [[ -z "$security_file" ]]; then
log_error "validate_command: .auto-claude-security.json not found. Run 'clawpinch.sh --init' to create default config."
return 1
fi
Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/helpers/common.sh
Line: 151:154

Comment:
Missing configuration file `.auto-claude-security.json` breaks validation. File is gitignored but required for `validate_command()` to work.

```suggestion
  if [[ -z "$security_file" ]]; then
    log_error "validate_command: .auto-claude-security.json not found. Run 'clawpinch.sh --init' to create default config."
    return 1
  fi
```

How can I resolve this? If you propose a fix, please make it concise.


# SECURITY: Validate config file ownership and permissions to prevent
# symlink attacks where an attacker replaces the config with a symlink
# to a file they control, overriding the allowlist.
local resolved_file
resolved_file="$(readlink -f "$security_file" 2>/dev/null || realpath "$security_file" 2>/dev/null || echo "$security_file")"

# Check file is owned by current user or root
local file_owner
if [[ "$(uname -s)" == "Darwin" ]]; then
file_owner="$(stat -f '%u' "$resolved_file" 2>/dev/null)" || file_owner=""
else
file_owner="$(stat -c '%u' "$resolved_file" 2>/dev/null)" || file_owner=""
fi

if [[ -n "$file_owner" ]]; then
local current_uid
current_uid="$(id -u)"
if [[ "$file_owner" != "$current_uid" ]] && [[ "$file_owner" != "0" ]]; then
log_error "validate_command: security config '$resolved_file' is owned by uid $file_owner (expected $current_uid or root). Possible symlink attack."
return 1
fi
fi

# Check file is not world-writable
if [[ "$(uname -s)" == "Darwin" ]]; then
local file_perms
file_perms="$(stat -f '%Lp' "$resolved_file" 2>/dev/null)" || file_perms=""
if [[ -n "$file_perms" ]] && [[ "${file_perms: -1}" =~ [2367] ]]; then
log_error "validate_command: security config '$resolved_file' is world-writable (mode $file_perms). Fix with: chmod o-w '$resolved_file'"
return 1
fi
else
if stat -c '%a' "$resolved_file" 2>/dev/null | grep -q '[2367]$'; then
log_error "validate_command: security config '$resolved_file' is world-writable. Fix with: chmod o-w '$resolved_file'"
return 1
fi
fi

# Check if jq is available
if ! has_cmd jq; then
log_error "validate_command: jq is required but not installed"
return 1
fi

# Validate the security config is valid JSON first
if ! jq '.' "$security_file" >/dev/null 2>&1; then
log_error "validate_command: $security_file is not valid JSON"
return 1
Comment on lines +228 to +286
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function walks from current directory to root searching for .auto-claude-security.json. This is good for monorepo support, but the search behavior isn't documented. If a user has the file in the wrong location (e.g., their home directory instead of project root), validation will find it but use potentially wrong allowlist. Consider adding a log_info message when the file is found to show which config is being used.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/helpers/common.sh
Line: 215:229

Comment:
The function walks from current directory to root searching for `.auto-claude-security.json`. This is good for monorepo support, but the search behavior isn't documented. If a user has the file in the wrong location (e.g., their home directory instead of project root), validation will find it but use potentially wrong allowlist. Consider adding a log_info message when the file is found to show which config is being used.

<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +284 to +286
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing config file causes validation to fail silently. When .auto-claude-security.json is not found, validate_command() logs an error and returns 1, but the calling code in clawpinch.sh:305 suppresses stderr with 2>/dev/null. Users won't know their commands are being rejected due to missing config — they'll just see auto_fix fields stripped with a warning.

Check for config file existence once at startup and show a clear setup message if missing, rather than failing on every command validation.

Prompt To Fix With AI
This is a comment left during a code review.
Path: scripts/helpers/common.sh
Line: 248:250

Comment:
Missing config file causes validation to fail silently. When `.auto-claude-security.json` is not found, `validate_command()` logs an error and returns 1, but the calling code in `clawpinch.sh:305` suppresses stderr with `2>/dev/null`. Users won't know their commands are being rejected due to missing config — they'll just see auto_fix fields stripped with a warning. 

Check for config file existence once at startup and show a clear setup message if missing, rather than failing on every command validation.

How can I resolve this? If you propose a fix, please make it concise.

fi

# Get all allowed commands from security config
local allowed_commands
allowed_commands="$(jq -r '
(.base_commands // []) +
(.stack_commands // []) +
(.script_commands // []) +
(.custom_commands // []) |
.[]
' "$security_file" 2>/dev/null)"

if [[ -z "$allowed_commands" ]]; then
log_warn "validate_command: allowlist is empty in $security_file — no commands are permitted"
return 1
fi
Comment on lines +291 to +302
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current logic for parsing the .auto-claude-security.json file incorrectly treats a valid, empty allowlist as a parsing error. An empty allowlist should be a valid configuration, meaning no commands are permitted. The current implementation checks if the jq output is empty, which is true for both a jq parsing error and a valid empty list of commands. This can be fixed by separating the JSON validity check from the command extraction and relying on jq's exit code to detect parsing errors. This makes the function more robust and correctly handles the empty allowlist case.

Suggested change
allowed_commands="$(jq -r '
(.base_commands // []) +
(.stack_commands // []) +
(.script_commands // []) +
(.custom_commands // []) |
.[]
' "$security_file" 2>/dev/null)"
if [[ -z "$allowed_commands" ]]; then
log_error "validate_command: failed to parse security config"
return 1
fi
if ! jq '.' "$security_file" >/dev/null 2>&1; then
log_error "validate_command: security config file '$security_file' contains invalid JSON."
return 1
fi
allowed_commands="$(jq -r '
(.base_commands // []) +
(.stack_commands // []) +
(.script_commands // []) +
(.custom_commands // []) |
.[]
' "$security_file")"


# Extract ALL commands from the string (split by |, &&, ||, ;)
# This ensures we validate every command in a chain
# Try to use Python script for proper quote-aware parsing
local script_dir
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
local parse_script="$script_dir/parse_commands.py"

# Require Python parser — fail closed if unavailable (no insecure fallback)
if ! [[ -f "$parse_script" ]] || ! has_cmd python3; then
log_error "validate_command: python3 or parse_commands.py not available. Cannot securely validate command."
return 1
fi

local base_commands_list
base_commands_list="$(python3 "$parse_script" "$cmd_string")"
if [[ $? -ne 0 || -z "$base_commands_list" ]]; then
log_error "validate_command: Python helper failed to parse command string."
return 1
fi

# Check each base command
while IFS= read -r base_cmd; do
# Skip empty lines
[[ -z "$base_cmd" ]] && continue

# Skip flags/options (start with -)
[[ "$base_cmd" =~ ^- ]] && continue

# Strip surrounding quotes from command token before validation
# (shlex may return quoted tokens like "'cmd'" — strip to get bare command)
base_cmd="${base_cmd#\'}"
base_cmd="${base_cmd%\'}"
base_cmd="${base_cmd#\"}"
base_cmd="${base_cmd%\"}"
[[ -z "$base_cmd" ]] && continue

# Block interpreters with command execution flags (-c, -e)
# e.g., "bash -c 'rm -rf /'" — bash is in allowlist but -c allows arbitrary code
case "$base_cmd" in
bash|sh|zsh|python|python3|perl|ruby|node)
if [[ "$cmd_string" =~ [[:space:]]-[ce][[:space:]] ]] || [[ "$cmd_string" =~ [[:space:]]-[ce]$ ]]; then
log_error "validate_command: interpreter '$base_cmd' with -c or -e flag is not allowed"
return 1
fi
;;
esac

# Check allowlist first (allows script_commands like ./clawpinch.sh)
if grep -Fxq -- "$base_cmd" <<< "$allowed_commands"; then
continue
fi

# Block path-based commands not in allowlist (/bin/rm, ./malicious, ~/script)
if [[ "$base_cmd" =~ ^[/~\.] ]]; then
log_error "validate_command: path-based command '$base_cmd' is not in the allowlist"
return 1
fi

# Command not in allowlist
log_error "validate_command: '$base_cmd' is not in the allowlist"
return 1
done <<< "$base_commands_list"

# All commands validated successfully
return 0
}

# ─── OS detection ───────────────────────────────────────────────────────────

detect_os() {
Expand Down
9 changes: 9 additions & 0 deletions scripts/helpers/interactive.sh
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ _confirm() {

_run_fix() {
local cmd="$1"

# NOTE: No separate validate_command() call here — safe_exec_command()
# performs its own comprehensive validation (blacklist + whitelist + per-command
# checks) which is stricter and handles redirections in safe patterns like
# "jq ... > tmp && mv tmp file.json". The allowlist-based validate_command()
# is used only in the AI remediation pipeline (clawpinch.sh).

printf '\n %b$%b %s\n' "$_CLR_DIM" "$_CLR_RST" "$cmd"
if safe_exec_command "$cmd" 2>&1 | while IFS= read -r line; do printf ' %s\n' "$line"; done; then
printf ' %b✓ Fix applied successfully%b\n' "$_CLR_OK" "$_CLR_RST"
Expand Down Expand Up @@ -562,6 +569,8 @@ auto_fix_all() {
f_id="$(echo "$fixable" | jq -r ".[$i].id")"
f_cmd="$(echo "$fixable" | jq -r ".[$i].auto_fix")"
printf ' [%d/%d] %s ... ' $(( i + 1 )) "$fix_count" "$f_id"

# safe_exec_command handles its own validation (whitelist + blacklist)
if safe_exec_command "$f_cmd" >/dev/null 2>&1; then
printf '%b✓ pass%b\n' "$_CLR_OK" "$_CLR_RST"
passed=$(( passed + 1 ))
Expand Down
Loading