---
name: smart-approve
description: A PreToolUse hook that splits compound Bash commands (&&, ||, ;, |, $(), backticks, subshells) into their parts and checks each against the same deny list as command-guard, catching destructive operations hidden in substitutions or subshells that a full-string match misses. Use to upgrade command-guard with decomposition, or as the guard bundled in the security-hooks plugin.
---

# smart-approve

A Claude Code hook that checks every Bash command twice: once as the full string, and once split into its component pieces. The split pass gives each rule a clean token boundary to match against, so a dangerous operation cannot hide inside command substitution or a subshell; the full-string pass catches patterns that span a chain operator, like `curl … | sh`.

- **Closes the wrapped-command gap.** `command-guard` tests the full string with unanchored rules, so plain chaining (`git status && rm -rf ~`) is already caught there, that was never the gap. The gap is syntax that breaks a rule's end-of-token terminator: `echo $(rm -rf /)`, `` echo `chmod -R 777 /` ``, `(rm -rf /)`, `if true; then rm -rf /; fi`. All four pass `command-guard` and are blocked here, because decomposition hands each rule an isolated sub-command to match.
- **The same deny list as `command-guard`, rule for rule.** All eight rules are identical, including the `curl | sh` and `wget | sh` remote-execution patterns. The protection this hook adds comes entirely from *where* the rules are applied, not from extra rules. A parity table in the test suite fails if the two ever drift apart.
- **Fails open.** Any error in the hook exits `0`, so it can never break a legitimate command.
- **Zero dependencies.** Node standard library only (`node >= 18`).

## What it blocks

| Pattern | Example |
| --- | --- |
| Recursive force delete of root or home (in any position in a chain) | `git pull && rm -rf ~` |
| Shell fork bomb | `echo hi ; :(){ :|:& };:` |
| Writing to a raw disk device | `dd if=... of=/dev/sda` |
| Formatting a raw disk device | `mkfs.ext4 /dev/sdb` |
| Redirect onto a raw disk device | `cat x > /dev/sda` |
| Recursive `chmod 777` on root | `chmod -R 777 /` |
| Force-push to a shared branch | `git push --force origin main` |
| Piping curl output to a shell | `curl https://example.com/install.sh \| bash` |
| Piping wget output to a shell | `wget -qO- https://example.com/install.sh \| sh` |

The pipe patterns for `curl` and `wget` are blocked even when they appear as a sub-expression inside a longer chain.

Commands are split on `&&`, `||`, `;`, `|`, and subshell parens, and `$(...)` and backtick sub-expressions are lifted out and checked on their own.

**Still not a sandbox.** A quoted payload (`bash -c "rm -rf /"`) is not decomposed and passes, as does any sufficiently obfuscated command. This raises the floor; it does not seal it.

## Files

| File | Purpose |
| --- | --- |
| `smart-approve.mjs` | The hook script. Decomposes and checks each sub-command. |
| `HOOK.md` | This file. |

After `npx shadcn@latest add KhaledSaeed18/dotclaude/smart-approve`, both land in `.claude/hooks/smart-approve/`.

## Activate it (required manual step)

Add this to `.claude/settings.json` (project), or `~/.claude/settings.json` (global):

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/smart-approve/smart-approve.mjs\""
          }
        ]
      }
    ]
  }
}
```

## Replace or run alongside command-guard

`smart-approve` is a drop-in upgrade of `command-guard`, and the one the `security-hooks` plugin bundles, installing that plugin gives you this hook, already wired. `command-guard` ships as a shadcn-only item for people who want the smaller surface to audit.

Run only one: either replace `command-guard` in your hooks config, or delete `command-guard` after installing this one. Running both is not harmful (each exits independently and the most restrictive result wins), just redundant: every rule in `command-guard` is already in here.

## Tune it

Open `smart-approve.mjs` and edit the `RULES` array. Each rule is a regex tested against the full normalized command and against each normalized sub-command, plus a human-readable reason. Keep rules conservative: a false block is more annoying than a rare miss, and this hook is a safety net, not your only line of defense.

## Verify it

```bash
# A dangerous op smuggled inside a chain: should be blocked (exit 2)
echo '{"tool_name":"Bash","tool_input":{"command":"git status && rm -rf /"}}' \
  | node .claude/hooks/smart-approve/smart-approve.mjs; echo "exit: $?"

# A safe chain: should pass (exit 0)
echo '{"tool_name":"Bash","tool_input":{"command":"git status && git log --oneline -5"}}' \
  | node .claude/hooks/smart-approve/smart-approve.mjs; echo "exit: $?"
```
