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.

"""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,
)
NoteVerified by replay

The nudge (transcript signals) and llm_gate (live model verdict) judgments are exercised by replaying real sessions, not the inline tests; only the deterministic hook and lint expectations below are asserted here.

What it catches

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

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

uvx capt-hook --hooks docs/examples test

See also