Skip to content
Ultracite
Esc
↑↓navigate↵open⌘Jpreview

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

opencode runOpenCode 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. 1

    The agent writes a file

    It writes var and any, without semicolons.

  2. 2

    The plugin fixes what it can

    ultracite fix turns var into const, formats the file, and adds the one problem it can’t fix to the tool result.

  3. 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.

  1. 01Always

    Session starts

    OpenCode puts every AGENTS.md from your working directory up to the repo root into the system prompt, or CLAUDE.md if there’s none.

    Ultraciteinit appends Ultracite’s rules to AGENTS.md.

  2. 02Always

    The agent edits

    The write and edit tools, or apply_patch for GPT models, save the file to disk.

    UltraciteThe rules in context mean the first draft starts closer to passing.

  3. 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.

  4. 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.

  5. 05With a plugin

    Plugins

    Plugins’ tool.execute.after hooks run with the tool’s result, and whatever they add to it goes to the model.

    UltraciteThe plugin runs ultracite fix and 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 .ts file 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 fix applies 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 npx for pnpm exec, yarn or bunx to 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 /lint and OpenCode runs ultracite 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, fix and doctor. init offers to install it into .agents/skills, which OpenCode reads, or add it yourself.

    npx skills add haydenbleasel/ultracite
  • LSP 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.md from your working directory up to the repo root into the system prompt, falls back to CLAUDE.md when there’s none, and adds your global ~/.config/opencode/AGENTS.md. ultracite init --agents opencode appends Ultracite’s rules to AGENTS.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: a tool.execute.after hook 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 whole ultracite fix and reports what’s left.
Does the plugin work with GPT models?
Yes. OpenCode gives GPT models an apply_patch tool instead of write and edit. The plugin reads the files a patch added, changed or moved from the tool’s metadata and skips deleted ones.
Does ultracite init set up the plugin?
Not yet. init writes the rules into AGENTS.md and 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 fix couldn’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.

Ship less slop

One command sets up your linter, formatter, editor and agents. Works with Oxlint, Biome and ESLint.