Skip to content

Explainer · 347 seconds

Hooks: stop a dangerous command before it runs

A PreToolUse command hook can refuse matching tool calls before they run. A blocking example, its limits, and how blocking differs from asking for human approval.

Edit in studio →

LinkedIn X

Transcript

  1. Stop a dangerous command before it runs.
  2. This illustrative cleanup deletes a file you never committed.
  3. A hook can block it. Exit two refuses the call.
  4. But if your hook crashes, the command can still run.
  5. Here's why, and how to test it.
  6. A hook sounds like one more step in the build.
  7. But this one runs before a matched tool call. That timing matters.
  8. It can refuse the call before execution.
  9. A PreToolUse command hook runs your program before a matching tool call, and reads its decision.
  10. Before the call. That's where this check belongs.
  11. Here's the harness. The repository underneath. The project file, the skills, the hook.
  12. The project file and the skills give instructions. The model can still misread them.
  13. The hook can enforce a refusal. Permissions and a sandbox provide other controls.
  14. That doesn't make the hook infallible. Its matching and failure handling still have to work.
  15. So we need to check what it blocks, and what happens when it breaks.
  16. Here's a concrete version, with the shell already permitted.
  17. You ask it to clean up the repo before a push. It plans to remove build output and untracked files.
  18. It runs git clean, dash f d x. That also removes a file you never committed.
  19. Git has no copy of an untracked file. The hook belongs here, before the command runs.
  20. Before a tool call matched by your configuration, the hook program runs.
  21. It gets a JSON object on standard input: which tool, and what arguments.
  22. The program reads it, decides, and returns a result.
  23. For an exit-code-only command hook, the code matters.
  24. First, the blocking path: exit two.
  25. Exit two blocks the call. Whatever you wrote to standard error comes back as the reason.
  26. Exit zero with no decision leaves the normal permission flow in charge. That may still ask you.
  27. Other exit codes can be non-blocking errors. Without failure blocking, normal permissions still decide.
  28. So an unexpected script error can leave your check out of the decision.
  29. A hook error notice is not a refusal. Check failure behavior in the version you run.
  30. Use two to refuse. Test failures too.
  31. The other half of this is knowing what to put behind it.
  32. Keep prompts useful. Start with actions whose consequences are hard to reverse.
  33. Git helps only when the work you need is saved in a verified checkpoint. Uncommitted work can still be lost.
  34. Prompt for every routine step and you train yourself to click through. Save attention for consequential changes.
  35. Here are five categories to assess. These are policy goals, not coverage our little example proves.
  36. Changes you can't easily undo.
  37. Anything recursive and deleting. Anything that rewrites shared history.
  38. Changes to a non-disposable database. Anything that deploys, publishes or sends.
  39. And actions that spend money. The sample doesn't detect all of these categories.
  40. Now look at the configuration. This wires up a script; it is not the whole safeguard.
  41. In the project's settings file, under hooks, PreToolUse.
  42. The Bash matcher covers shell calls, including shell reads. It doesn't cover every other tool.
  43. The command points to a separate script. The book shows a small shell example.
  44. It checks literal command patterns. A match is refused. Plenty of other forms are missed.
  45. Now follow a matching command through an illustrative refusal.
  46. The agent proposes it. The hook matches it. Exit two blocks that call.
  47. Show this as blocked. There is no human approval prompt in this example.
  48. A refusal ends that tool call. It doesn't sit waiting for a yes.
  49. Actual approval needs a different result: a structured permission decision of ask.
  50. In an interactive session that requests confirmation. In a non-interactive run, an unanswered prompt can be denied.
  51. For our blocking example, the useful part is the reason returned to the agent.
  52. For example: blocked, git clean removes untracked files. Commit or stash first, then try again.
  53. So, what to do this afternoon.
  54. Keep the check small enough to review. Raw command prefixes do not make a safe allow list.
  55. Test malformed input, missing dependencies and timeouts with harmless fixtures. The book example still needs failure handling.
  56. Test the actual commands you use. Add alternate spellings and shell chains. Do not mistake a pattern list for a sandbox.
  57. And scope credentials and sandbox access. A local pattern check alone can't protect production.
  58. That's the useful distinction: blocking a match is not the same as asking a person.
  59. Before the call. Explicit refusal. Tested failures and scoped access.
  60. Stop a dangerous command before it runs.
  61. What would your hook block first?

Read the full article →

Comments

    No comments yet.

    Comments show after I review them. No email or account needed. To limit spam, a scrambled form of your IP address is kept for 10 minutes.

    Questions or feedback?
    I reply to every note.

    Say hi