# A complete hooks file

> Compose a deterministic safety block and a live-state workflow gate in one `.claude/hooks/` file.

Hooks rarely live alone. A real `.claude/hooks/` directory mixes a safety block that fires the instant the agent runs a command with a workflow gate that watches session state. This capstone composes two primitives in one file: [block_command](../../reference/primitives.md#captain_hook.block_command) (deterministic, carries inline tests) and [gate](../../reference/primitives.md#captain_hook.gate) (judges live session state). One file listens on `PreToolUse`, `Stop`, and `SubagentStop` at once.

``` python
"""Compose a safety block and a workflow gate in one hooks file."""

from __future__ import annotations

from captain_hook import (
    Allow,
    Block,
    Input,
    RanCommand,
    TouchedFile,
    block_command,
    gate,
)

# A complete .claude/hooks/ file: a safety block and a workflow gate composing in one
# place.

# 1. Safety: never let the agent force-push.
block_command(
    r"git\s+push\s+--force(?!-)",
    reason="Force-push rewrites shared history",
    hint="Use --force-with-lease",
    tests={
        Input(command="git push --force origin main"): Block(),
        Input(command="git push --force-with-lease"): Allow(),
    },
)

# 2. Workflow: don't finish a Python change without running the tests.
# RanCommand matches parsed argv prefixes (launcher-literal), so each spelling
# of the test runner gets its own entry -- skip_if is OR.
gate(
    "You edited Python files but never ran the tests. Run `uv run pytest` before finishing.",
    only_if=[TouchedFile("**/*.py")],
    skip_if=[RanCommand("uv", "run", "pytest"), RanCommand("pytest")],
)
```

> **Note: Verified by replay**
>
> The [gate](../../reference/primitives.md#captain_hook.gate) reads live session state -- which files were touched and whether the tests ran -- so its judgment is exercised by replaying a real session, not by the inline tests.


# What it catches

``` bash
git push --force origin main   # block_command: force-push rewrites shared history
```

The [gate](../../reference/primitives.md#captain_hook.gate) also blocks `Stop`/`SubagentStop` when you edited `**/*.py` but never ran `pytest`, judged from live session state rather than a single input.


# What it allows

``` bash
git push --force-with-lease   # --force-with-lease is safe; not a bare --force
```

*The block / allow split mirrors the hook inline `tests`, so it stays true as the hook evolves.*


# Run it yourself

``` bash
uvx capt-hook --hooks docs/examples test
```


# See also

- [Compose primitives](../../docs/guide/primitives.md)
- [Enforce a multi-step workflow](../../docs/guide/state.md#enforce-a-multi-step-workflow)
- [Test hooks inline](../../docs/guide/testing.md)
