Lint every file OpenCode writes
OpenCode reads Ultracite’s rules from AGENTS.md before it writes. A one-file plugin runs ultracite fix after every edit and hands the model what’s left, so it fixes the problem in the same turn instead of in review.
ForOpenCodeby AnomalyTested on 1.18.35
> build
← Write src/parse.ts
Wrote file successfully.
Ultracite fixed what it could. Fix these, then re-read the file before editing it again:
src/parse.ts:1:30: error typescript(no-explicit-any): Unexpected `any`. Specify a different type. help: Use `unknown` instead, this will force you to explicitly, and safely, assert the type is correct.
Finished in 31ms on 1 files using 10 threads.
← Edit src/parse.ts
@@ -1,4 +1,4 @@
-export const parse = (input: any) => {
+export const parse = (input: string): unknown => {
const value = JSON.parse(input);
return value;
};
Done.
1
The agent writes a file
It writes
varandany, without semicolons.2
The plugin fixes what it can
ultracite fixturnsvarintoconst, formats the file, and adds the one problem it can’t fix to the tool result.3
The agent fixes the rest
The model reads that result before its next step, and the file passes
ultracite check.
What happens when OpenCode writes a file
OpenCode has three places a linter can plug in. Two of them are off by default, and only one tells the model about every problem it leaves.
01Always
Session starts
OpenCode puts every
AGENTS.mdfrom your working directory up to the repo root into the system prompt, orCLAUDE.mdif there’s none.Ultracite
initappends Ultracite’s rules toAGENTS.md.02Always
The agent edits
The
writeandedittools, orapply_patchfor GPT models, save the file to disk.UltraciteThe rules in context mean the first draft starts closer to passing.
03Off by default
Formatters
Prettier, Oxfmt or Biome run on the file. Their output is thrown away, and Biome’s only formats: it doesn’t apply lint fixes.
UltraciteLeave them off. The plugin runs the whole
ultracite fix.04Off by default
LSP diagnostics
With
"lsp": true, Oxlint’s, Biome’s or ESLint’s language server reports on the file. Errors are added to the result, up to 20; warnings are dropped.UltraciteEvery Ultracite rule is an error, so none of them are dropped.
05With a plugin
Plugins
Plugins’
tool.execute.afterhooks run with the tool’s result, and whatever they add to it goes to the model.UltraciteThe plugin runs
ultracite fixand adds what’s left.
The plugin: one file, every edit
OpenCode doesn’t run Claude Code-style hooks, but its plugins see every tool call. This one closes the loop the way Ultracite’s hooks do in Claude Code and Copilot.
Drop it in
OpenCode loads every
.tsfile in.opencode/plugins/when a session starts, and installs the types for you.Only the edited files
It skips reads, searches and shell commands, and runs only on the files an edit touched, so a check takes about a third of a second instead of a full-project pass.
Fix, then report
ultracite fixapplies safe fixes and formats. When problems remain it exits non-zero, and the plugin appends them to the tool result the model reads next.Your runner
Swap
npxforpnpm exec,yarnorbunxto match your package manager.
import type { Plugin } from "@opencode-ai/plugin";
// The tools that write files: `write` and `edit` take a `filePath`; GPT
// models get `apply_patch`, which lists what it touched in its metadata.
const EDIT_TOOLS = new Set(["apply_patch", "edit", "write"]);
export const Ultracite: Plugin = async ({ $ }) => ({
"tool.execute.after": async (input, output) => {
if (!EDIT_TOOLS.has(input.tool)) {
return;
}
const files =
input.tool === "apply_patch"
? (output.metadata?.files ?? [])
.filter((file) => file.type !== "delete")
.map((file) => file.movePath ?? file.filePath)
: [input.args.filePath];
const result = await $`npx ultracite fix ${files}`.nothrow().quiet();
if (result.exitCode !== 0) {
// The tool's output goes back to the model, so this is what it reads.
output.output += `\n\nUltracite fixed what it could. Fix these, then re-read the file before editing it again:\n${result.stdout}${result.stderr}`;
}
},
});More ways to wire it in
None of these are required. Each one gives the agent another way to reach the same standard.
A /lint command
Type
/lintand OpenCode runsultracite check, pastes the output into the prompt and asks for fixes.--- description: Run Ultracite and fix what it reports --- Ultracite check output: !`npx ultracite check` Fix every problem above, then run `npx ultracite check` again until it passes.The Ultracite skill
Ultracite’s skill teaches the agent
init,check,fixanddoctor.initoffers to install it into.agents/skills, which OpenCode reads, or add it yourself.npx skills add haydenbleasel/ultraciteLSP diagnostics
Turn on language servers to also get Oxlint’s, Biome’s or ESLint’s errors after each edit, up to 20 per file.
{ "$schema": "https://opencode.ai/config.json", "lsp": true }Permissions
Bash runs without asking by default. If you’ve locked it down, let Ultracite through; the last matching rule wins.
{ "$schema": "https://opencode.ai/config.json", "permission": { "bash": { "*": "ask", "npx ultracite *": "allow" } } }
OpenCode questions, answered
- Does OpenCode read AGENTS.md or CLAUDE.md?
- Both, in that order. OpenCode puts every
AGENTS.mdfrom your working directory up to the repo root into the system prompt, falls back toCLAUDE.mdwhen there’s none, and adds your global~/.config/opencode/AGENTS.md.ultracite init --agents opencodeappends Ultracite’s rules toAGENTS.md, so they’re in context from the first message. - Does OpenCode run Claude Code hooks?
- No. OpenCode ignores the hooks in
.claude/settings.json, and the request to support them is still open. Plugins cover the same ground: atool.execute.afterhook runs after every tool call and can add to the result the model reads, which is what Ultracite’s plugin does. - Why not use OpenCode’s built-in formatters?
- Its formatters are off by default, and when they’re on, OpenCode throws away their output, so the model never hears about a problem they couldn’t fix. The built-in Biome formatter also runs
biome format --write, which formats without applying lint fixes. The plugin runs the wholeultracite fixand reports what’s left. - Does the plugin work with GPT models?
- Yes. OpenCode gives GPT models an
apply_patchtool instead ofwriteandedit. The plugin reads the files a patch added, changed or moved from the tool’s metadata and skips deleted ones. - Does
ultracite initset up the plugin? - Not yet.
initwrites the rules intoAGENTS.mdand offers to install the Ultracite skill. The plugin is one file you add to.opencode/plugins/; OpenCode installs its types the first time it loads it. - What does the model see when a problem is left?
- Each problem
ultracite fixcouldn’t settle, with its file, line, rule and a hint. On Oxlint it’s one line per problem with no code frame or summary: Oxlint notices it’s running under OpenCode and switches to a format built for agents, so the feedback costs few tokens.
Keep going
- DocsAgent rulesWhat
initwrites for each agent, and how to keep it current. - DocsAgent skillsThe Ultracite skill: setup, commands and troubleshooting.
- DocsAgent hooksThe post-edit hooks init writes for other agents, and what each one does.
- AgentsAll agentsEvery agent Ultracite sets up, and what it writes for each.
Ship less slop
One command sets up your linter, formatter, editor and agents. Works with Oxlint, Biome and ESLint.