Skip to content
Weaveweave / docs
Get started ↗
Docs/Guides/Fleet mods

Fleet mods

Small add-ons an agent writes for you that change what Fleet shows and, from Fleet 0.51.0, what you send, what an agent may ask, and which buttons and panes you get. Turn them on, try a sample, ask for one, and write one by hand.

A mod is a small add-on that changes what Fleet shows. You don’t have to write one. You describe what you want, and an agent writes it for you.

A mod can draw on a tool’s row in the conversation: on the row’s line, and in the body you see when you open the row. From Fleet 0.51.0 it can also change a prompt before the agent gets it, queue a follow-up when a turn ends, and refuse an agent’s permission ask. Buttons in anything a mod draws work, a mod can open a pane of its own beside the conversation, and a mod can list a pane in the + menu. A band above the composer and a chip in the status bar are not available yet.

See what mods can do in the showcase, with a prompt for each.

Fleet’s mods follow Claude Code’s mods, and the names are the same where the idea is the same. They work with OpenCode, OpenCode 2 and Claude Code sessions.

  1. Open Settings → Features.
  2. Turn on Mods. It is marked Experimental and is off by default.

Mods run in a small program that needs Bun. Fleet asks before it does anything:

  • If Fleet finds a Bun on your computer, it says where. Choose Use my Bun to run mods on it. Nothing is downloaded.
  • Otherwise, choose Turn on and install, or Download Fleet’s own if it found a Bun but you’d rather not use it. Fleet downloads Bun, checks it against a checksum built into Fleet and shows a progress bar. Nothing else on your computer changes.
The Mods switch in Settings, Features, with a progress bar for the Bun download

With Mods off, nothing changes. No mod program runs and no Bun is downloaded.

Fleet comes with twelve sample mods. From Fleet 0.52.0 they’re in the Showcase under Settings → Mods, grouped by what they change: tool rows; prompts, turns and permission asks; buttons and panes. Each one says what it does and where to try it. Choose Add and it’s kept like any mod you keep, with its switch in the list above. Once Mods are on, Settings → Features links to the Showcase too.

The Showcase in Settings, Mods: sample mods grouped under Tool rows and Prompts, turns and permission asks, each with a description, a Try it line, and Add or Added

In Fleet 0.50.0 and 0.51.0 the samples are links under the Mods switch in Settings → Features instead: Try the test-chips sample, Try build-summary and so on. 0.50.0 has only test-chips.

To start with test-chips, add it, then ask an agent to run your tests in any session. The tool’s row shows how many tests passed, failed and were skipped. Open the row and the failing test names come first, then the full output.

A bash row for bun test with the chips 1 passed, 1 failed and 1 skipped

It reads dotnet test, bun test, vitest, pytest and go test.

Describe what you want in a session. For example: “Show how many lines a command printed on its row.” The agent writes a draft.

  • A draft runs only in that session, wherever the session is open: desktop, phone or another tab. What it draws is marked Draft.
  • When the agent asks you to keep it, Fleet shows what the mod touches: what it draws, what it reads, and whether it uses the network or files.
  • Choose Keep for all my sessions to make it a version (v1, then v2, and so on) that runs in all your sessions on this machine. Choose Discard to tell the agent you didn’t keep it. The draft stays on in that session.
The review dialog, Keep bash-line-count, listing what it draws, reads and changes, with Keep for all my sessions and Discard

Settings → Mods lists the mods you kept. Each has a version, an on/off switch and Undo. Undo goes back one version, or turns the mod off if it is on v1. Nothing is deleted.

Settings, Mods, with two kept mods, each with a switch and an Undo link

Three hooks let a mod do more than draw. All three work with OpenCode, OpenCode 2 and Claude Code, except where noted. They never reach files, processes or the network.

The prompt.submit hook sees what you sent before the agent gets it. A mod can do one of two things:

  • Reword it. The new text is what the conversation shows in your bubble and what the agent receives, and the bubble is marked Rewritten by the mod’s name.
  • Add context only the model reads. The note isn’t in your bubble. OpenCode, OpenCode 2 and Claude Code take it. Pi doesn’t, so Pi sessions get none. Added context is capped at 8 KiB in total.

A mod can also read the session’s git branch with $.session.branch(). It is null outside a git repository or on a detached HEAD.

The branch-context sample adds the branch as a note when a prompt mentions branches, PRs, pushing or merging. Here the user typed only “which branch am I on?”, and the agent answered from the note without running a command.

A session where the user asked which branch they are on and the agent answered feature/lantern-sync

Limits:

  • It fails open. If no mod hooks it, the host isn’t running, a hook throws or fails to answer in time, or the answer isn’t usable, your prompt goes on exactly as you typed it, with no context. Fleet waits at most 2 seconds, and a hook has 1.5 seconds of its own.
  • Slash commands (/compact, a skill, a command) are not sent through it. Every other prompt is, including a new session’s first message and prompts from automations.
  • A mod can’t drop or block a prompt yet.

The turn.complete hook fires when a turn ends, whether it finished, you stopped it or it failed. Its event says which (isAborted, isFailed) and lists changedFiles: the files the agent’s edit, write and patch calls finished since the last turn ended. OpenCode and OpenCode 2 report them. Claude Code and Pi always send an empty list, and edits made by a shell command such as sed are not seen. Side conversations (/btw) and the child sessions an agent delegates to don’t fire it.

A hook can call $.prompt.submit({ text }) to queue a message in the session. It joins the same queue your own messages wait in, so it shows in the queue list and is sent next, behind anything you had already queued.

The tests-after-edits sample queues “Run the tests” when a turn changed files and wasn’t stopped or failed. A turn that changed no files queues nothing. Below, the user had queued a message of their own, so the mod’s “Run the tests” waits in the queue as Next.

A session with a Run the tests message waiting as Next above the composer

Limits:

  • A mod can queue 3 follow-ups in a row in a session. The 4th is refused until you send or queue a message in that session. Fleet keeps the count in memory, so a restart starts it again.
  • Fleet waits at most 2 seconds for the hooks, then goes on with the queue. When a message a mod queued is sent, its bubble is marked Sent by the mod’s name.
  • turn.complete is watch only. agent, model, usage and cost are in the types but Fleet doesn’t send them yet.

The permission.ask hook sees an agent’s ask before you do. A mod can deny it with a reason, and then you get no card and no Needs-you. Fleet answers the ask for you with the message <mod name>: <reason>, so the agent is told who refused and why. OpenCode and Claude Code show that as the tool call’s error. OpenCode 2 is sent the message but its server drops it, so the agent sees a plain rejection there. A reason is up to 500 characters.

The push-guard sample denies git push while the session is on main, with the reason “Don’t push from main. Make a branch and push that instead.”

A git push row showing push-guard's reason, and the agent explaining that the push was blocked because the session is on main

Limits:

  • A mod only sees asks that would reach you. If Settings → Permissions is set to allow shell commands, they are never asked, so push-guard never sees them. Set it to ask for shell commands (ask or edits) to use it.
  • A mod can deny or pass. It can’t allow an ask for you yet.
  • It fails open: if no mod hooks it, the host isn’t running, a hook fails or takes more than 2 seconds, the ask goes to you as normal.
  • Pi has no asks. A subagent’s ask is sent on the parent session, where you’d see its card.

From 0.51.0 a Button, Input or Select in anything a mod draws reaches the mod: on a tool row, in a pane, anywhere. Press the button and Fleet runs the mod’s callback in the mod’s program. If the callback writes $.state, Fleet draws that place again, with no page load.

The press-counter sample puts a Count a press button and a count in the opened body of a shell row. Below it has been pressed twice.

An opened Bash row with a Count a press button and the text Pressed 2 times above the command's output

Limits:

  • A button’s callback runs in the mod. If the mod has stopped or reloaded since the row was drawn, the press is not handled and Fleet draws the row again so you get a fresh button. A callback that throws counts as a failure of the mod, like any hook.
  • An Input sends at most 4 changes a second, the last value kept.

A pane is a tab a mod draws in a session’s content panel, next to Changes and Files. A mod gets one two ways.

A callback calls $.ui.open({ id, title }), usually from a button. Fleet opens the tab (and the panel if it was closed), brings it forward, and asks the mod’s Pane hook to draw it. $.ui.close({ id }) closes it. Opening the same id again draws it in place.

The release-board sample adds an Open board button to a shell row. Pressing it opens a Release board tab, drawn by the mod, with a Close button that closes it.

A session with the Release board tab open beside Files: three items marked Done, Running and To do, and a Close button

A pane is a full tree. Buttons, inputs and $.state work in it like anywhere else.

A mod can also declare panes in mod.json. You open them from the + menu in the content panel, under From mods: the pane’s icon and title, and the mod’s name as the hint (Open while its tab is open). Picking one opens the tab and the mod draws it. The phone’s session menu lists them too, and the pane opens in a sheet.

The + menu with Built in items, then From mods: Release board from release-board, and Test summary marked Draft from test-digest
  • A draft’s pane has a Draft mark and is listed only in the session that wrote it.
  • When a mod is off, its panes stay in the menu with an Off hint. Its open tab says the mod is off and has a Turn on button, the same switch as Settings → Mods. Turn it on and the pane draws again.
The Release board tab saying release-board is off, with a Turn on button
  • The review before you keep a mod has an Adds row: “A pane in the + menu” and the pane titles.
The Keep test-digest review: Adds, A pane in the + menu: Test summary; Changes, Nothing the agent sees; Network none; Files none

Limits:

  • Pane tabs don’t come back after you reload the page. Open them again from the + menu.
  • A $.ui.open sent while no browser has the session open is lost. It is not replayed when you open the session later.
  • Open and close panes from a button or an event, not while drawing. A ui.render hook that calls $.ui.open or $.ui.close throws. A $.ui.open in session.start runs again every time the mod host restarts, so open from a user’s press instead.
  • A mod can open a pane it never declared. It draws the same way but isn’t in the menu.
  • A pane’s id belongs to its mod: only the opening mod’s Pane hooks draw it, and two mods may use the same id.

A mod can compute and draw. In Fleet 0.50.0 that is all it can do. From 0.51.0 a mod can also reword your prompts, add context for the model, queue follow-up messages, refuse an agent’s permission asks, run a callback when you press its button and open a pane, so read the review before you keep one that hooks them. A mod still can’t:

  • read or write files
  • start processes
  • use the network
  • answer a permission ask for you (it can only deny, or pass)

Fleet draws everything a mod returns with its own components, in its own theme. A mod never runs script in Fleet’s page. The review before Keep lists every call the mod makes.

Mods are not a sandbox. They run in one program beside Fleet, as you, so keep only mods you’ve read the review for.

If a mod breaks Fleet:

  • Start without mods. Add ?mods=off to any Fleet address, or use the menu item. Mods stay stopped until Fleet restarts or you choose Turn mods back on. It only affects your own mods.
  • Three strikes. If a mod fails three times in a row, Fleet turns it off. The error shows in Settings → Mods. A draft is turned off in its session until the agent reloads it.
  • Undo works even on a mod that was turned off by strikes.

You can ask an agent and never read this section. If you’d rather write a mod yourself, here is the short version. The full contract is docs/mods/api.md in the Fleet repository, and the types are in mods/types/fleet-mods.d.ts.

A mod is a folder with two files.

test-chips/mod.json
{
"name": "test-chips",
"version": "0.1.0",
"description": "Draws test runs as passed, failed and skipped counts",
"hooks": "./mod.ts"
}

The name is lowercase letters, digits and -, and matches the folder. A mod that offers panes adds "panes": [{ "id": "board", "title": "Release board", "icon": "gauge" }] (up to 8; id is letters, digits, _ and -; title is 1 to 40 characters; icon is one of the Icon names). The module exports register. It can import type from fleet-mods and nothing else.

test-chips/mod.ts
import type { Register } from "fleet-mods";
export const register: Register = (on) => {
on("ui.render", { component: "ToolUse", props: { tool: "bash" } }, async ($, e, next) => {
// observe: return next(e)
// answer: return a tree (or null) without calling next
return next(e);
});
};
  • on(event, matcher, hook) registers a hook. Call it inside register, with a string event and an object matcher.
  • A hook is ($, e, next). It can pass the event on with next(e), change it with next({ ...e, ... }), or answer with a tree.
  • $ is the only way out. Call it in full, like $.ui.resolve(e).
Event Fires when Needs
session.start A mod’s first event in a session, and again after the mod reloads 0.50.0
ui.render Fleet is about to draw a tool row the mod hooked 0.50.0
prompt.submit You sent a prompt, before the agent gets it. A mod can reword it or add context for the model 0.51.0
turn.complete A turn ended: finished, stopped or failed. Watch only, but a hook can queue a follow-up with $.prompt.submit 0.51.0
permission.ask An agent asked for something you’d have to answer, before you see it. A mod can deny it with a reason 0.51.0
ui.press A Button the mod drew is pressed. A hook can pass it on, or swallow it 0.51.0
ui.input An Input changes or is submitted 0.51.0
ui.select A Select changes 0.51.0
Place Where it draws Needs
ToolUse On a tool row’s line. Inline elements only 0.50.0
ToolResult The body a tool row shows when opened 0.50.0
ComposerBand A band above the composer Not yet
StatusChip A chip in the status bar. Inline elements only Not yet
Pane A pane the mod opened with $.ui.open or declared in mod.json. Tell panes apart by e.requestId, the pane’s id 0.51.0

Inline elements are Text, Pill, Icon, Button and a row Box holding only those.

$.mod, $.ui (resolve, invalidate, log, and open and close from 0.51.0), $.state, $.store, $.session (including branch() from 0.51.0), $.prompt (submit, from 0.51.0) and $.clock. $.ui.open and $.ui.close open and close a pane. $.ui.toast and $.session.surfaces() are in the contract but don’t do anything yet. There is no file, process or network access, and no setTimeout: use $.clock.

Box, Text, Pill, Icon, Button, Input, Select, Markdown, Code and Page. Colours are roles (text, muted, accent, good, warn, bad), not hex values, so a mod follows the theme. Every Button, Input, Select and Page needs a unique key.

This is test-chips cut down to one runner. It lives in full in the Fleet repository at mods/samples/test-chips/.

test-chips/mod.ts
import type { Register } from "fleet-mods";
const TEST_COMMAND = /\b(bun (run )?test|vitest|pytest|go test)\b/;
// bun test prints " 12 pass", " 2 fail" and " 1 skip" on their own lines.
function parse(output: string) {
const pass = /^\s*(\d+) pass\s*$/m.exec(output);
const fail = /^\s*(\d+) fail\s*$/m.exec(output);
if (!pass && !fail) return undefined;
const skip = /^\s*(\d+) skip\s*$/m.exec(output);
const failing = [...output.matchAll(/^\(fail\)\s+(.+?)\s*$/gm)].map((m) => m[1]);
return { passed: Number(pass?.[1] ?? 0), failed: Number(fail?.[1] ?? 0), skipped: Number(skip?.[1] ?? 0), failing };
}
export const register: Register = (on) => {
on("ui.render", { component: ["ToolUse", "ToolResult"], props: { tool: ["bash", "shell"] } }, async ($, e, next) => {
if (e.component !== "ToolUse" && e.component !== "ToolResult") return next(e);
if (!TEST_COMMAND.test(String(e.props.input.command ?? ""))) return next(e);
const counts = parse(e.props.output);
if (!counts) return next(e);
const { Box, Text, Pill } = $.ui.resolve(e);
const pills = Box({
flexDirection: "row",
gap: 1,
children: [
Pill({ tone: "good", label: `${counts.passed} passed` }),
counts.failed > 0 && Pill({ tone: "bad", label: `${counts.failed} failed` }),
counts.skipped > 0 && Pill({ tone: "neutral", label: `${counts.skipped} skipped` }),
],
});
if (e.component === "ToolUse") return pills;
// Opened: the failing names, then Fleet's own output under them.
return Box({
flexDirection: "column",
gap: 2,
children: [
...counts.failing.map((name) => Text({ code: true, color: "bad", children: [name] })),
await next(e),
],
});
});
};

The hook runs for bash and shell rows, skips anything that isn’t a test command, and draws chips on the row. When the row is opened it draws the failing names and then await next(e), which is Fleet’s own output.

A mod that reads something it shouldn’t, or draws something Fleet doesn’t know, doesn’t load or draws nothing, and counts a strike. The mod’s log in Settings → Mods says why.

Still not available: a band above the composer, a chip in the status bar, toasts ($.ui.toast), $.session.surfaces(), slash commands a mod adds, dropping a prompt, pane tabs coming back after a reload, and allow for permission asks. Mods that read files, run processes or use the network are planned, and the review before you keep a mod will show what it touches.

Questions or something that doesn’t work? Open an issue in the Fleet repository.