# Code quality

> Catch `print()` calls and bare `except:` clauses in Python edits, escalating from structural checks to a behavioral nudge to an LLM gate.

The agent keeps slipping `print()` into production modules instead of your logger, and bare `except:` clauses show up under deadline pressure. You want layered feedback, not one blunt rule. This page composes four primitives on `Event.PostToolUse`: two structural checks fire on every edit, a behavioral nudge reacts when the agent's narration repeats the antipattern, and an LLM gate adjudicates the ambiguous prod-print cases.

``` python
"""Catch print() calls and bare except clauses in Python edits across four escalating tiers."""

from __future__ import annotations

import ast
import re
from collections.abc import Iterator

from captain_hook import (
    Allow,
    Event,
    Input,
    Pattern,
    Signal,
    Signals,
    SourceEdits,
    TestFile,
    Warn,
    hook,
    lint,
    llm_gate,
    nudge,
)


def bare_excepts(node: ast.AST) -> Iterator[str]:
    if isinstance(node, ast.ExceptHandler) and node.type is None:
        yield f"line {node.lineno}"


hook(
    Event.PostToolUse,
    only_if=[SourceEdits(lang="py"), Pattern("print($$$)")],
    skip_if=[TestFile()],
    message="Use the project logger instead of print(). See docs/logging.md.",
    tests={
        Input(tool="Edit", file="src/app.py", content='import sys\nprint("debug")\n'): Warn(pattern="logger"),
        Input(tool="Edit", file="src/app.py", content="logger.info('ok')\n"): Allow(),
        Input(tool="Edit", file="src/app.py", content="label = 'print(x)'  # only in a string\n"): Allow(),
    },
)


lint(
    bare_excepts,
    message="Bare except clauses silently swallow errors. Catch a specific exception type instead: {violations}",
    tests={
        Input(tool="Edit", file="src/app.py", content="try:\n    f()\nexcept:\n    pass\n"): Warn(pattern="swallow"),
        Input(tool="Edit", file="src/app.py", content="try:\n    f()\nexcept ValueError:\n    pass\n"): Allow(),
    },
)


nudge(
    "You keep adding print()s after edits. Switch to logger.debug() and tail the log.",
    signals=Signals(
        patterns=[
            Signal(pattern=r"print\(", weight=1, flags=re.MULTILINE),
            Signal(pattern=r"debug[\s_-]print", weight=2, flags=re.IGNORECASE),
        ],
        threshold=3,
        window=8,
    ),
)


llm_gate(
    "Does this diff add a print() that should be a logger call, where the surrounding "
    "module already imports a logger? Block only if the prod print is unambiguous.",
    message="Replace print() with logger: {reasoning}",
    events=Event.PostToolUse,
    only_if=[SourceEdits(lang="py"), Pattern("print($$$)")],
    skip_if=[TestFile()],
    max_fires=2,
)
```

> **Note: Verified by replay**
>
> The [nudge](../../reference/primitives.md#captain_hook.nudge) (transcript signals) and [llm_gate](../../reference/primitives.md#captain_hook.llm_gate) (live model verdict) judgments are exercised by replaying real sessions, not the inline tests; only the deterministic [hook](../../reference/registration.md#captain_hook.hook) and [lint](../../reference/primitives.md#captain_hook.lint) expectations below are asserted here.


# What it catches

``` python
print("debug")              # real print() call in a prod module -- use the logger
try:
    f()
except:                     # bare except silently swallows every error
    pass
```


# What it allows

``` python
logger.info('ok')           # logger call, no print()
label = 'print(x)'          # 'print(' only inside a string, not a real call
try:
    f()
except ValueError:          # catches a specific exception type
    pass
```

*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

- [Primitives](../../docs/guide/primitives.md)
- [LLM hooks](../../docs/guide/llm-hooks.md)
- [Style guide checks](../../docs/guide/primitives.md#style-rules)
