kosa8 / Guides

Guide · Claude Code hooks

How do I block dangerous commands with Claude Code hooks?

Updated

Add a PreToolUse hook with the matcher Bash to .claude/settings.json. Claude Code sends the hook the tool call as JSON on stdin, and a hook that exits with code 2 blocks the call and shows Claude its stderr as the reason. Hooks and permission rules both read command text, so a reworded command can get past them; for a limit that holds, also run Claude Code inside a sandbox.

01Steps

  1. Write the hook script. The command is in tool_input.command. This one needs jq. Make it executable with chmod +x.

    .claude/hooks/block-dangerous.sh
    #!/bin/sh
    cmd=$(jq -r '.tool_input.command // ""')
    case "$cmd" in
      *"rm -rf"*|*"git push --force"*|*"git push -f"*|*"git reset --hard"*|*"git clean -f"*|*"terraform destroy"*)
        echo "Blocked by a hook: $cmd. Ask the user to run it." >&2
        exit 2 ;;
    esac
    exit 0
  2. Register it in settings. Put it in .claude/settings.json for the project, ~/.claude/settings.json for every project, or managed settings for an organisation.

    .claude/settings.json
    {
      "hooks": {
        "PreToolUse": [
          {
            "matcher": "Bash",
            "hooks": [
              { "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-dangerous.sh" }
            ]
          }
        ]
      }
    }
  3. Test it without Claude. Pipe in the JSON Claude Code would send. Exit code 2 means blocked.

    terminal
    echo '{"tool_name":"Bash","tool_input":{"command":"rm -rf build"}}' | .claude/hooks/block-dangerous.sh; echo $?
    Blocked by a hook: rm -rf build. Ask the user to run it.
    2
  4. Ask instead of block where a person should decide. Exit 0 and print JSON with permissionDecision set to ask, deny or allow, and a reason.

    hook output
    {
      "hookSpecificOutput": {
        "hookEventName": "PreToolUse",
        "permissionDecision": "ask",
        "permissionDecisionReason": "Deletes files outside build/"
      }
    }
  5. Back the hook with permission rules. Deny and ask rules are checked whatever a hook returns, so a hook cannot approve what a deny rule refuses.

    .claude/settings.json
    {
      "permissions": {
        "deny": ["Bash(git push --force *)", "Bash(terraform destroy *)"],
        "ask": ["Bash(rm *)", "Bash(git clean *)", "Bash(git reset *)"]
      }
    }
  6. Put a boundary under the rules. Run Claude Code in a kosa8 microVM that sees one folder. The claude kit adds its own pre-run hook, and the sandbox limits what any command the rules miss can reach.

    terminal
    curl -fsSL https://kosa8.com/install.sh | sh
    kosa8 agent run -w ~/code/api claude

02Can a Claude Code hook be bypassed?

Yes. A hook that matches command text only catches the spellings it was written for. The script above misses rm -r -f, find . -delete, a delete inside a script file and one inside a Python one-liner.

Claude Code's documentation makes the same point about permission rules: Bash(rm *) does not stop /bin/rm -rf build/ or bash -c 'rm -rf build/', and Bash(git push *) does not stop git -C . push origin main. It also warns that a hook exiting with code 1 is a non-blocking error, so a hook that crashes, for example because jq is missing, lets the call through. Use exit 2 for anything meant to enforce a policy.

The hook file lives in a folder the agent can edit. Claude Code treats .claude as a protected path and prompts before writing to it, but bypassPermissions mode skips that prompt.

03Should I use hooks or permission deny rules?

Use both. Deny and ask rules are simple and take precedence: Claude Code checks deny, then ask, then allow, and a matching deny rule blocks the call even if a hook returned allow. Hooks add logic rules cannot express, such as checking a URL or a path inside the command.

For a hard limit on files and network that does not depend on command text, Claude Code's documentation points to its sandboxing. Organisations can set allowManagedHooksOnly in managed settings so only administrator hooks run.

04What does kosa8 add under Claude Code hooks?

A separate machine and rules that parse commands. In the claude kit, every tool call is checked before it runs: a download piped into a shell or an edit to the agent's own settings or hooks is refused, and a force push, git reset --hard or terraform destroy waits for kosa8 mcp approve.

Because the shell command is parsed, cd repo && git push -f, bash -c 'git push -f' and git -C repo push -f count as the same force push. kosa8's docs report 66 of 75 risky calls stopped in a hand-labelled set and 1 of 89 ordinary calls, and list the misses: a make target, a script file, an alias, a program in a variable and code inside a one-liner. If the check cannot run, the call is refused. Every call the model asked for is written to the audit log either way (kosa8 audit ls).

05Other options

  • Claude Code's built-in /sandbox. OS-level limits on shell commands (Seatbelt on macOS, bubblewrap on Linux and WSL2). The simplest boundary to add under hooks.
  • Claude Code permission modes. Manual mode prompts before Bash commands outside a built-in read-only set unless a rule allows them; permissions.disableBypassPermissionsMode stops anyone from skipping prompts.
  • Docker or Docker Sandboxes. Run Claude Code in a container with only the project mounted.
  • gVisor, Kata Containers or Firecracker. Stronger isolation for teams that already run Linux infrastructure.
  • A full VM in UTM or Lima. A separate machine for long-lived work.

06Sources