# Conditions composition

> Target one precise edit by stacking conditions instead of writing one sprawling regex.

Most real hooks fire on several facts at once: the right tool, the right file, the right content, and not the wrong directory. Cram that into a single pattern and it turns unreadable and brittle. This example composes primitives so each fact is its own condition, AND-ed with `only_if` and carved out with `skip_if`.

``` python
"""Flag raw SQL written into application Python by composing conditions."""

from __future__ import annotations

from captain_hook import (
    Allow,
    Content,
    Event,
    FilePath,
    Input,
    SourceEdits,
    Warn,
    hook,
)

# Compose conditions to target exactly the right edit. only_if is AND, skip_if is
# any-skips, and SourceEdits already narrows to non-test source in one language.
# Here: a raw SQL string written into application Python, but not into migrations
# (skipped by path) and not into tests (SourceEdits excludes test files by default).
hook(
    Event.PostToolUse,
    message="Raw SQL in application code. Route queries through db/queries.py.",
    only_if=[SourceEdits(lang="py", paths="app/**"), Content(r"(?i)\bselect\b.+\bfrom\b")],
    skip_if=[FilePath("**/migrations/**")],
    tests={
        Input(tool="Edit", file="app/users.py", content='q = "SELECT id FROM users"\n'): Warn(),
        Input(tool="Edit", file="app/migrations/0001.py", content='"SELECT 1 FROM t"\n'): Allow(),
        Input(tool="Edit", file="app/tests/test_users.py", content='"SELECT id FROM users"\n'): Allow(),
        Input(tool="Edit", file="app/util.py", content="x = 1  # nothing to flag here\n"): Allow(),
    },
)
```


# What it catches

``` python
Edit app/users.py: q = "SELECT id FROM users"  # raw SQL in application code, not a migration or test
```


# What it allows

``` python
Edit app/migrations/0001.py: "SELECT 1 FROM t"      # migrations excluded by skip_if FilePath("**/migrations/**")
Edit app/tests/test_users.py: "SELECT id FROM users"  # test files excluded by SourceEdits default
Edit app/util.py: x = 1  # nothing to flag here       # no SELECT ... FROM content match
```

*The catch / 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

- [Filter hooks with conditions](../../docs/guide/primitives.md#filter-with-conditions)
- [Conditions cheatsheet](../../docs/reference/conditions.md)
- [Matchers reference](../../docs/reference/matchers.md)
