Back to catalogue
ValidationTaskCompleted
Test suite gate before task completion
No task closes on a red build
Blocks a task from being marked as completed if the test suite fails. The agent receives the test output as feedback and must fix the failures before it can close the task, enforcing a green-build policy for every unit of work.
What does the Test suite gate before task completion hook do?
Test suite gate before task completion is a Claude Code TaskCompleted hook. It fires automatically at that lifecycle event — outside the model, so it can't be skipped or forgotten. No task closes on a red build.
Because it is a deterministic Node.js script, it executes on every matching event without relying on the model to remember — the guarantee that makes agentic workflows safe to automate.
Use cases
- Prevent agents from closing tasks with broken tests in multi-agent pipelines
- Enforce a green-build policy without human review for every agent task
- Surface test failures back to the agent as actionable feedback
Tags
#tasks#tests#quality-gate#agent-teams#ci
settings.json fragment
{
"hooks": {
"TaskCompleted": [
{
"hooks": [
{
"command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/task-completed-test-gate.mjs",
"type": "command"
}
]
}
]
}
}Script · .claude/hooks/task-completed-test-gate.mjs
#!/usr/bin/env node
// @hookstack task-completed-test-gate
import { execSync } from "node:child_process";
// @hookstack task-completed-test-gate
// Bloque la complétion d'une tâche si les tests échouent (TaskCompleted)
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
function defaultExec(cmd) {
return execSync(cmd, { stdio: "pipe", timeout: 120_000 });
}
// Détecte le gestionnaire de paquets depuis le lockfile (cohérent avec enforce-package-managers).
function detectManager({
exists = existsSync,
projectDir = process.cwd(),
} = {}) {
if (exists(join(projectDir, "pnpm-lock.yaml"))) return "pnpm";
if (
exists(join(projectDir, "bun.lockb")) ||
exists(join(projectDir, "bun.lock"))
)
return "bun";
if (exists(join(projectDir, "yarn.lock"))) return "yarn";
return "npm";
}
export function run(
input,
{
exec = defaultExec,
exists = existsSync,
projectDir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd(),
} = {},
) {
try {
exec(`${detectManager({ exists, projectDir })} test --if-present 2>&1`);
return null;
} catch (e) {
const out = (e.stdout ?? e.stderr ?? e.message).toString().slice(0, 800);
return {
exitCode: 2,
message: `Tests must pass before completing "${input.task_subject}".\n${out}`,
};
}
}
/* v8 ignore next 6 */
if (process.argv[1] === fileURLToPath(import.meta.url)) {
const input = JSON.parse(readFileSync(0, "utf8"));
const result = run(input);
if (result) {
process.stderr.write(result.message);
process.exit(result.exitCode);
}
}