Back to catalogue
ValidationPreToolUse· Bash
Enforce build wrapper
Forces ./gradlew / ./mvnw, kills bare-tool drift
Blocks bare gradle/mvn commands when the project ships a wrapper (gradlew/mvnw), keeping the build toolchain version pinned for every contributor and agent.
What does the Enforce build wrapper hook do?
Enforce build wrapper is a Claude Code PreToolUse hook matching Bash. It fires automatically at that lifecycle event — outside the model, so it can't be skipped or forgotten. Forces ./gradlew / ./mvnw, kills bare-tool drift.
As a PreToolUse hook it runs before the action completes, so it can block or adjust what Claude is about to do. 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
- Pin the Gradle/Maven version via the wrapper
- Avoid toolchain drift between machines
- Standardize build invocation across a team
Tags
#java#gradle#maven#wrapper#build-tool#validation#workflow
settings.json fragment
{
"hooks": {
"PreToolUse": [
{
"hooks": [
{
"command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/enforce-java-build.mjs",
"type": "command"
}
],
"matcher": "Bash"
}
]
}
}Script · .claude/hooks/enforce-java-build.mjs
#!/usr/bin/env node
// @hookstack pre-bash-enforce-java-build
// Bloque `gradle`/`mvn` nus quand le wrapper (gradlew/mvnw) existe — le wrapper
// épingle la version du toolchain pour toute l'équipe (PreToolUse Bash).
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
const FORBIDDEN = [
{ re: /(^|[;&|\s`])gradle(\s|$)/, tool: "gradle", wrapper: "gradlew" },
{ re: /(^|[;&|\s`])mvn(\s|$)/, tool: "mvn", wrapper: "mvnw" },
];
// Retire le contenu des chaînes entre guillemets pour éviter les faux positifs
// quand gradle/mvn apparaissent comme valeurs d'arguments texte (ex. git commit -m
// "...gradle...") tout en continuant à bloquer les vraies invocations.
function stripQuotedArgs(cmd) {
return cmd
.replace(/"(?:[^"\\]|\\.)*"/g, '""')
.replace(/'(?:[^'\\]|\\.)*'/g, "''");
}
export function run(
input,
{ exists = existsSync, projectDir = process.env.CLAUDE_PROJECT_DIR ?? process.cwd() } = {},
) {
if (input.tool_name !== "Bash") return null;
const cmd = stripQuotedArgs(input.tool_input?.command ?? "");
const hit = FORBIDDEN.find(({ re }) => re.test(cmd));
if (!hit) return null;
// On n'impose le wrapper que s'il existe réellement dans le projet.
if (!exists(join(projectDir, hit.wrapper))) return null;
return {
decision: "block",
reason: `Use './${hit.wrapper}' instead of the bare '${hit.tool}' — the wrapper pins this project's ${hit.tool === "gradle" ? "Gradle" : "Maven"} version.`,
};
}
/* v8 ignore next 5 */
if (process.argv[1] === fileURLToPath(import.meta.url)) {
const input = JSON.parse(readFileSync(0, "utf8"));
const result = run(input);
if (result) process.stdout.write(JSON.stringify(result));
}