Automating Dart Linting in Claude with PostToolUse#
The "linting ping-pong" is a frustrating reality of AI-assisted coding. An LLM generates code, you copy it, and then you spend five minutes fixing commas, types and style violations. For Flutter and Dart developers using Claude, this cycle ends now. I’ve found that Claude's
PostToolUse hooks allow for a "Silent Reviewer"—a process that formats and analyzes code before you even see it.
Claude PostToolUse: Shifting from Prompting to Enforcement#
Most developers use prompt engineering to solve code quality issues. Instructions like "Follow the project's analysis options" are helpful, but they are soft constraints. LLMs can ignore them when the context becomes complex.
Claude's hook system creates a hard boundary. Instead of asking the agent to be careful, the environment enforces quality. The
PostToolUse hook runs after a tool executes but before the result reaches the model. If the code is flawed, the hook blocks execution. This forces the agent to fix its mistakes immediately.
Configuring the PostToolUse Hook in Claude#
The integration begins in the .claude/settings.json file. This is where the PostToolUse
hook is registered and mapped to specific tools.
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/after_edit.sh"
}
]
}
]
}
}
With matcher: "Write|Edit", the hook triggers whenever Claude modifies a file. The script
after_edit.sh becomes the engine of this automation.
Building the "Silent Reviewer" for Dart#
The after_edit.sh script is fast and non-intrusive. It intercepts the file path, verifies it is a Dart file, and runs the standard toolchain.
1. Intercepting the Tool Input#
The script receives a JSON payload from Claude containing the tool name and file path. Using jq, it extracts these variables:
INPUT=$(cat)
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
2. Formatting and Analysis#
If the file is a Dart file, the script runs fvm dart format for stylistic consistency. It then executes
dart analyze to catch static errors.
if [[ -n "$FILE_PATH" && "$FILE_PATH" == *.dart && -f "$FILE_PATH" ]]; then
fvm dart format "$FILE_PATH" > /dev/null 2>&1
OUTPUT=$(fvm dart analyze "$FILE_PATH" 2>&1)
# ... logic for handling output
fi
Enforcing Quality with the "Block" Decision#
The most critical part is how the script communicates back to Claude. If dart analyze finds issues, the script returns a JSON object with a
decision: "block".
jq -n \
--arg output "$OUTPUT" \
--arg tool "$TOOL_NAME" \
--arg file "$FILE_PATH" \
'{
decision: "block",
reason: ("The tool \($tool) was executed, but dart analyze found issues in \($file):\n\($output)"),
systemMessage: "⚠️ Lint issues found after \($tool)"
}'
When Claude receives a "block" decision, it interprets the linting errors as a tool failure. The model must analyze the error messages, fix the code and try again. This creates a self-correcting loop. It continues until the code perfectly aligns with your
analysis_options.yaml.
Boosting Developer Velocity with Automated Linting#
This isn't just about clean code; it's about velocity. When the environment enforces quality:
- Zero Technical Debt: Code enters the repository in a finished state. No "fix lints later" tasks are ever created.
- Reduced Context Switching: You don't have to break your flow to fix the AI's formatting mistakes.
- Trust: I am convinced that you can trust the agent with complex refactors. It simply cannot proceed if it breaks the build or violates a lint rule.
Moving validation from the prompt to the toolchain changes your relationship with the AI. It stops being a generator of "almost-correct" code. Instead, it becomes a reliable engineering partner that respects your codebase.