|
| 1 | +# AI agents |
| 2 | + |
| 3 | +A coding agent does not know your team's style, and with open-java-format it does not have to. |
| 4 | +Format every file the agent writes, and the code arrives in the one style, whichever model wrote |
| 5 | +it. This page explains why that check comes first, what it cannot tell you, and how to set it up at |
| 6 | +four points: after each edit, in the agent's instructions, before a commit and in CI. |
| 7 | + |
| 8 | +## Why formatting comes first |
| 9 | + |
| 10 | +Every check between an edit and a merge answers its own question, and each one needs more than the |
| 11 | +one before it. |
| 12 | + |
| 13 | +| Check | What it tells you | What it needs | |
| 14 | +| --- | --- | --- | |
| 15 | +| Formatter | The file parses, and it is laid out in the one style | The file | |
| 16 | +| Compiler | The code type-checks | The module and its dependencies | |
| 17 | +| Static analysis | No known bug pattern matched | Usually a compiled module | |
| 18 | +| Tests | The behaviour under test still holds | A build that runs | |
| 19 | +| Review | The change is the right one | A person's time | |
| 20 | + |
| 21 | +The formatter is first in that chain for four reasons. |
| 22 | + |
| 23 | +- **It needs one file.** There is no build, no classpath and no project model. Halfway through a |
| 24 | + refactoring the project does not compile, but every file the agent has touched can still be |
| 25 | + formatted. |
| 26 | +- **It is fast.** The native binary has no JVM to start. On an Apple silicon laptop the whole |
| 27 | + [hook](#after-every-edit-a-claude-code-hook) below takes about 50 ms for a 270-line file and |
| 28 | + about 0.6 s for a 4,000-line one, so it can run after every single edit. |
| 29 | +- **It parses the file.** A file with a syntax error fails at once, with the file, line and column. |
| 30 | + The agent hears about a missing semicolon before it spends a build on it. |
| 31 | +- **There is nothing to configure.** There is no style to describe in a prompt and no option for a |
| 32 | + model to get wrong. The output depends only on the input. |
| 33 | + |
| 34 | +## What the formatter does not tell you |
| 35 | + |
| 36 | +A formatter does not find bugs. open-java-format checks that a file parses and lays it out. It does |
| 37 | +not resolve a single type or symbol. |
| 38 | + |
| 39 | +``` java |
| 40 | +public class Typo { |
| 41 | + int f() { |
| 42 | + return "text"; |
| 43 | + } |
| 44 | + |
| 45 | + void g() { |
| 46 | + undefinedMethod(); |
| 47 | + } |
| 48 | +} |
| 49 | +``` |
| 50 | + |
| 51 | +Neither method compiles, and the formatter exits with 0. The compiler, static analysis, tests and |
| 52 | +review still have all of their work to do. Formatting first only means that they get code in one |
| 53 | +shape, and that a reviewer's diff shows what changed in the logic. |
| 54 | + |
| 55 | +## Set it up |
| 56 | + |
| 57 | +The four layers back each other up, so use as many as you can. |
| 58 | + |
| 59 | +| Layer | Runs | Misses | |
| 60 | +| --- | --- | --- | |
| 61 | +| [Claude Code hook](#after-every-edit-a-claude-code-hook) | After every file the agent edits | Files the agent changes through a shell command | |
| 62 | +| [AGENTS.md](#in-the-agents-instructions-agentsmd) | When the agent follows its instructions | Whatever the model forgets: an instruction is context, not enforcement | |
| 63 | +| [pre-commit hook](#before-a-commit-the-pre-commit-hook) | Before every commit | Machines where nobody installed it, and commits made with `--no-verify` | |
| 64 | +| [CI](#in-ci-the-last-gate) | On every pull request and push | Nothing that reaches a pull request | |
| 65 | + |
| 66 | +### After every edit: a Claude Code hook |
| 67 | + |
| 68 | +[Claude Code hooks](https://code.claude.com/docs/en/hooks-guide) run a command at fixed points of a |
| 69 | +session. A `PostToolUse` hook on the `Edit` and `Write` tools runs after every file the agent |
| 70 | +changes, and it gets the tool call as JSON on its standard input. |
| 71 | + |
| 72 | +The hook needs `open-java-format` on the `PATH`, see [Command line](get-started/command-line.md), |
| 73 | +and [`jq`](https://jqlang.org/). |
| 74 | + |
| 75 | +``` sh title=".claude/hooks/format-java.sh" |
| 76 | +#!/bin/sh |
| 77 | +# Claude Code runs this after every Edit and Write and passes the tool call as JSON on stdin. |
| 78 | +file=$(jq -r '.tool_input.file_path // empty') |
| 79 | + |
| 80 | +case "$file" in |
| 81 | + *.java) ;; |
| 82 | + *) exit 0 ;; |
| 83 | +esac |
| 84 | + |
| 85 | +# Exit code 2 makes Claude Code show the formatter's message to the model. |
| 86 | +open-java-format --ojf --skip-removing-unused-imports --replace "$file" || exit 2 |
| 87 | +``` |
| 88 | + |
| 89 | +``` sh |
| 90 | +chmod +x .claude/hooks/format-java.sh |
| 91 | +``` |
| 92 | + |
| 93 | +``` json title=".claude/settings.json" |
| 94 | +{ |
| 95 | + "hooks": { |
| 96 | + "PostToolUse": [ |
| 97 | + { |
| 98 | + "matcher": "Edit|Write", |
| 99 | + "hooks": [ |
| 100 | + { |
| 101 | + "type": "command", |
| 102 | + "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/format-java.sh", |
| 103 | + "args": [] |
| 104 | + } |
| 105 | + ] |
| 106 | + } |
| 107 | + ] |
| 108 | + } |
| 109 | +} |
| 110 | +``` |
| 111 | + |
| 112 | +Commit both files, and everyone who opens the project in Claude Code gets the hook. To keep it to |
| 113 | +yourself, put the `hooks` block into `.claude/settings.local.json` instead. |
| 114 | + |
| 115 | +**Unused imports stay for now.** An agent often adds an import in one edit and the code that uses |
| 116 | +it in the next. A full format after the first edit would delete that import, so the hook passes |
| 117 | +`--skip-removing-unused-imports`. The pre-commit hook and CI run the full check, and they catch the |
| 118 | +imports that really are unused. |
| 119 | + |
| 120 | +**A file that does not parse goes back to the agent.** The formatter leaves the file as it is and |
| 121 | +prints the error. The script then exits with 2, the exit code that makes Claude Code show a hook's |
| 122 | +message to the model, so Claude sees it right after its edit: |
| 123 | + |
| 124 | +``` text |
| 125 | +src/main/java/com/example/Broken.java:5:22: error: ';' expected |
| 126 | +``` |
| 127 | + |
| 128 | +The hook does not see a file that the agent rewrites through a shell command such as `sed -i`. The |
| 129 | +later layers cover those. |
| 130 | + |
| 131 | +### In the agent's instructions: AGENTS.md |
| 132 | + |
| 133 | +[AGENTS.md](https://agents.md/) is a Markdown file in the repository root that holds instructions |
| 134 | +for coding agents. Add a section that matches how the project runs the formatter. |
| 135 | + |
| 136 | +=== "Command line" |
| 137 | + |
| 138 | + ```` markdown title="AGENTS.md" |
| 139 | + ## Java formatting |
| 140 | + |
| 141 | + Java code in this repository is formatted with open-java-format. It has one style and no |
| 142 | + options, so never lay out code by hand and never try to match the lines around your change. |
| 143 | + |
| 144 | + After you create or edit a `.java` file, format it: |
| 145 | + |
| 146 | + ```sh |
| 147 | + open-java-format --ojf --replace path/to/File.java |
| 148 | + ``` |
| 149 | + |
| 150 | + Before you commit, run this check. It must print nothing, so format every file it lists: |
| 151 | + |
| 152 | + ```sh |
| 153 | + open-java-format --ojf --dry-run --set-exit-if-changed $(git ls-files '*.java') |
| 154 | + ``` |
| 155 | + ```` |
| 156 | + |
| 157 | +=== "Gradle plugin" |
| 158 | + |
| 159 | + ```` markdown title="AGENTS.md" |
| 160 | + ## Java formatting |
| 161 | + |
| 162 | + Java code in this repository is formatted with open-java-format. It has one style and no |
| 163 | + options, so never lay out code by hand and never try to match the lines around your change. |
| 164 | + |
| 165 | + After you change Java code, and again before you commit, run: |
| 166 | + |
| 167 | + ```sh |
| 168 | + git add -N . && ./gradlew formatDiff |
| 169 | + ``` |
| 170 | + ```` |
| 171 | + |
| 172 | + `formatDiff` reads `git diff HEAD`, which leaves out files that git does not track yet. |
| 173 | + `git add -N .` marks the files the agent created, so that they are formatted too. |
| 174 | + |
| 175 | +An instruction is context, not enforcement. A model can forget it in a long session, which is what |
| 176 | +the hook above and the two checks below are for. |
| 177 | + |
| 178 | +Claude Code [reads `AGENTS.md`](https://code.claude.com/docs/en/memory#agents-md) from version |
| 179 | +2.1.277 on, as long as the project has no `CLAUDE.md`. If it has one, import the file there with a |
| 180 | +line that says `@AGENTS.md`. |
| 181 | + |
| 182 | +### Before a commit: the pre-commit hook |
| 183 | + |
| 184 | +An agent that commits runs the repository's git hooks like anyone else. The |
| 185 | +[pre-commit hook](get-started/github-actions.md#git-pre-commit-hook) checks the staged `.java` |
| 186 | +files, stops the commit when one is not formatted and prints the command that fixes it, which is |
| 187 | +all an agent needs to recover. |
| 188 | + |
| 189 | +### In CI: the last gate |
| 190 | + |
| 191 | +An agent that works in the cloud and opens a pull request runs none of your local hooks. The |
| 192 | +[GitHub Action](get-started/github-actions.md#check-pull-requests-and-pushes) checks every pull |
| 193 | +request, whoever or whatever wrote it, and it needs no Java on the runner. |
0 commit comments