HookStackGitHub
Back to catalogue
ValidationStopStopWhen the agent finishes its task· non-blocking

Run pytest at end of response

Won't hand back until pytest is green

When the agent finishes, runs the full pytest suite via uv. If tests fail, Claude re-enters and receives the failure output as context to fix the regressions.

What does the Run pytest at end of response hook do?

Run pytest at end of response is a Claude Code Stop hook. It fires automatically at that lifecycle event — outside the model, so it can't be skipped or forgotten. Won't hand back until pytest is green.

Use cases

  • Regression safety net on every agent turn
  • TDD loop: write code until tests go green
  • Catch side-effects of refactors before control returns to the user

Tags

#validation#pytest#uv#python#tests#ci

settings.json fragment

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/pytest.mjs",
            "type": "command"
          }
        ]
      }
    ]
  }
}

Script · .claude/hooks/pytest.mjs

#!/usr/bin/env node
// Exécute pytest à la fin d'une session Python (Stop)
import { existsSync } from 'fs';
import { spawnSync } from 'child_process';
import { fileURLToPath } from 'url';

const PYTHON_MARKERS = ['pyproject.toml', 'setup.py', 'pytest.ini', 'setup.cfg'];

export function run({
  exists = existsSync,
  spawn = spawnSync,
  cwd = process.env.CLAUDE_PROJECT_DIR ?? process.cwd(),
} = {}) {
  const isPython = PYTHON_MARKERS.some(f => exists(`${cwd}/${f}`));
  if (!isPython) return null;

  const result = spawn('uv', ['run', 'pytest', '--tb=short', '-q'], {
    encoding: 'utf8',
    timeout: 120_000,
    cwd,
    stdio: ['ignore', 'pipe', 'pipe'],
  });

  const out = (result.stdout ?? '') + (result.stderr ?? '');
  const status = result.status ?? 1;
  const message = status !== 0
    ? `[pytest] ÉCHEC (exit ${status})\n${out.slice(-2000)}\n`
    : `[pytest] ✓ Tests passés\n${out.split('\n').filter(Boolean).slice(-5).join('\n')}\n`;

  return { status, message };
}

/* v8 ignore next 6 */
if (process.argv[1] === fileURLToPath(import.meta.url)) {
  const result = run();
  if (result) {
    process.stderr.write(result.message);
    if (result.status !== 0) process.exit(2);
  }
}