Step-by-step guide

Add a deterministic checkpoint

Put repeatable policy at the exact moment in the lifecycle where it matters.

You will build
A lifecycle checkpoint that can stop unsafe work.
Time
15 minutes
Before you start
A shell command or script that exits predictably.

What this changes

Some decisions should not depend on whether the model remembers an instruction. A checkpoint lets your own process inspect an event and return a clear verdict to the runtime.

  1. 01
    Consistent enforcement

    The same check runs every time the relevant moment occurs.

  2. 02
    Policy your team owns

    Use the systems and standards already trusted by your organization.

  3. 03
    Visible failure behavior

    A blocked action explains why it stopped instead of disappearing into model behavior.

01

Minimum setup

Start with one working configuration.

Select the lifecycle event, narrow it to the calls that matter, then return one JSON verdict on standard output. Use a hook when the check must run regardless of what the model remembers.

~/.clarvis/settings.json hook
{
  "hooks": [
    {
      "event": "pre_tool_use",
      "match": { "tool": "shell" },
      "command": "echo '{\"kind\":\"advise\",\"message\":\"a shell command is about to run\"}'",
      "timeout_ms": 5000,
      "on_failure": "pass"
    }
  ]
}
Why this is the minimum

The block above is the whole file. Settings are parsed as strict JSON, so a comment line anywhere in it discards the entire scope silently; keep the scope note out of the file and put it in ~/.clarvis/settings.json for yourself, or in <workspace>/.clarvis/settings.json to version it with the repository. Keep stdout exclusively for the verdict and send diagnostics to stderr. Choose on_failure deliberately: pass keeps work moving when the hook breaks; deny fails closed at events that are allowed to block.

02

Working patterns

Adapt it to work you actually do.

Open a pattern to see the complete files and the reason behind each choice.

Pattern 01 Block a recursive delete before it runs Configuration

You are happy for the agent to use the shell, but a recursive delete is one mistake you do not want to recover from. You want the call refused before it happens, with a message that tells the model what to do instead. Name both command-running tools: shell is the obvious one, and monitor_start takes the same command argument and runs it through the shell too, so a matcher that lists only shell leaves it open.

Block a recursive delete before it runs example
File: .clarvis/settings.json

{
  "hooks": [
    {
      "event": "pre_tool_use",
      "match": {
        "tool": ["shell", "monitor_start"],
        "args": { "command": "\\brm\\s+(-[A-Za-z]+\\s+)*-[A-Za-z]*[rR]" }
      },
      "command": "echo '{\"kind\":\"deny\",\"message\":\"Recursive rm is blocked in this workspace. Delete named paths one at a time, or ask me to run it myself.\"}'",
      "on_failure": "deny"
    }
  ]
}

The filter selects, the command decides. Because match.args already narrows to
the calls you care about, the command has nothing left to work out and can be a
single echo. `on_failure: "deny"` means that even if the command somehow fails to
run, the call is still refused.
//
The pattern is a JavaScript regular expression tested against the `command`
argument. It matches `rm -rf x`, `rm -r x`, `rm -fr x` and `rm -i -rf x`, and does
not match `rm file.txt`, `npm run build` or `confirm removal`. It does match
`git rm -r pkg`, which you may or may not want.
//
This is scoping, not a security boundary: a determined command can be spelled to
slip past any pattern. For enforceable shell policy, configure the guard.
Pattern 02 Refuse to finish while the tests are failing Configuration

Agents like to declare victory. You want the run's own test suite to be the thing that decides when the work is done, and you want the failure message to reach the model so it goes back and fixes the code rather than arguing.

Refuse to finish while the tests are failing example
File: .clarvis/hooks/tests-must-pass.sh

#!/bin/sh
# stdout is the verdict channel: nothing but the JSON below may be written there.
if bun test >&2; then
  echo '{"kind":"pass"}'
else
  echo '{"kind":"deny","message":"The test suite is red. Run bun test, fix what fails, then finish."}'
fi

File: .clarvis/settings.json

{
  "hooks": [
    {
      "event": "pre_finalize",
      "command": "sh .clarvis/hooks/tests-must-pass.sh",
      "timeout_ms": 600000,
      "on_failure": "deny"
    }
  ]
}

Three things make this work. The runner's own output is redirected to stderr, so
the only thing on stdout is the verdict. The script exits 0 in both branches, so a
red suite is reported as a deny carrying your message rather than as a hook
failure carrying a generic one. And timeout_ms is raised well above the 30000
millisecond default for this event, which no real suite finishes inside.
//
The hook fires for sub agents as well as the lead, so a delegated agent has to
clear the same bar. On a run where you want to hand back known-failing work, edit
the hook out and re-approve the workspace, or move the run to a workspace that
does not declare it.
03

Verify the behavior

Check the boundary, not just the happy path.

Use a small disposable task first. Confirm what works and what is deliberately unavailable before relying on the configuration in a real workflow.

  1. 01

    The hook fires only for the selected event and matcher.

  2. 02

    The allowed path produces a pass or no output and the run continues.

  3. 03

    The refused path returns a useful reason to the agent without executing the pending action.

Next guide

Package the capability

Move related skills, agents, hooks, and integrations into one reviewable directory.