HookStack
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);
	}
}

Learn more

Related hooks