Stop an opencode session before it spends more than you meant to.
An agent left to grind on a hard problem does not stop at the amount you had in mind — it stops when the task is done, or when it gives up. A refactor you expected to cost a couple of dollars can run all evening and hand you the bill afterwards, and a session that spawns sub-agents multiplies that quietly, in parallel, out of sight.
This plugin puts a ceiling on it. Every session gets a spend limit, the sidebar
shows how much of it is gone, toasts warn you on the way up, and the run is
cancelled at the line rather than after it. Sub-agents bill to the session you
are actually looking at, so nothing spends behind your back. When you decide the
work was worth more, /budget +2 raises the limit and puts the run back on its
feet.
-
Hard stop. When a session reaches its limit, the next turn is cancelled before any tokens are spent, and an in-flight turn that blows past the limit mid-run is aborted.
-
Warnings. One-off toasts at 50%, 80% and 95% of the limit, so spend is visible before it becomes a wall.
-
Sidebar block. A progress bar, amount spent, and percentage used, rendered directly under the built-in Context block.
-
Resume. Raising the limit after a run was cut short drops
continueinto the prompt box, so picking the work back up is one keypress — and the wording stays yours. -
Slash command.
/budgetto inspect,/budget <amount>to set,/budget +<amount>to top up,/budget offto remove the limit,/budget resetto fall back to the default.
- opencode 1.18 or newer.
- macOS or Linux. Windows is not supported.
opencode plugin opencode-budget-limitThis package exposes two entry points — a server plugin (the enforcement) and a
TUI plugin (the sidebar block) — and opencode keeps those in two different
config files. opencode plugin reads the package's exports, notices both
targets, and writes each one into the right file for you: the server entry into
opencode.json and the TUI entry into tui.json.
Then add the slash command at ~/.config/opencode/commands/budget.md:
---
description: Show or change the spend limit for this session
---
budget $ARGUMENTSRestart opencode.
If you would rather wire it up yourself, both files need editing. Neither one is
a fallback for the other — the TUI never reads opencode.json, and the server
never reads tui.json.
~/.config/opencode/opencode.json:
{
"$schema": "https://opencode.ai/config.json",
"plugin": ["opencode-budget-limit"]
}~/.config/opencode/tui.json:
{
"$schema": "https://opencode.ai/tui.json",
"plugin": ["opencode-budget-limit"]
}Listing the package in only one file gives you half the plugin: enforcement without the sidebar, or nothing at all.
| Command | Effect |
|---|---|
/budget |
Show spend, limit, and how much is left |
/budget 5 |
Set this session's limit to $5.00 |
/budget +2 |
Raise this session's limit by $2.00 |
/budget off |
Remove the limit for this session |
/budget reset |
Drop the override; fall back to the default |
Limits apply to the root session. A sub-agent spawned from a session shares that session's budget rather than getting a fresh one.
| Variable | Default | Meaning |
|---|---|---|
OPENCODE_BUDGET_DEFAULT |
1 |
Default limit, in dollars, for sessions with no override |
OPENCODE_BUDGET_WARN |
0.5,0.8,0.95 |
Comma-separated fractions of the limit that trigger a warning toast |
OPENCODE_BUDGET_FILE |
(see below) | Absolute path to the limits file |
OPENCODE_BUDGET_DEBUG |
unset | Set to 1 to write a debug log next to the limits file |
Limits live in:
$XDG_DATA_HOME/opencode/budget-limit.json
falling back to ~/.local/share/opencode/budget-limit.json when XDG_DATA_HOME
is unset. OPENCODE_BUDGET_FILE overrides the whole path.
The server and the sidebar resolve this location through the same helper
(src/budget-file.js), which is the only reason they are guaranteed to agree on
one file. If you are patching this package, do not inline that logic.
The format:
{
"version": 2,
"default": 1,
"sessions": {
"ses_abc123": { "limit": 5, "updatedAt": 1755600000000 },
"ses_def456": { "limit": "unlimited", "updatedAt": 1755600000000 }
}
}"default" is the limit for any session without an entry. A session's limit
is either a number of dollars or the string "unlimited". Entries untouched for
30 days are pruned on startup.
Earlier versions kept this file at
~/.config/opencode/plugins/budget-limit.json. That path is still read when the
new one is missing, and the next write migrates it, so upgrading does not lose
existing limits.
Alongside it sits budget-spend.json, written by the server and read by the
sidebar:
{
"version": 1,
"sessions": {
"ses_abc123": { "spent": 0.8156, "subagents": true, "updatedAt": 1755600000000 }
}
}It is a cache, not state — delete it and the next turn rebuilds it. The total is recorded under every session in the branch, so opening the sidebar on a sub-agent session shows the budget that governs it. Entries are pruned on the same 30-day schedule as limits.
A limit applies to the root session and every sub-agent session spawned from
it, because all of them are billed. The host's Context block reports one
session's own cost, so it reads lower whenever sub-agents ran — a session that
spent $0.39 itself while its sub-agents spent $0.43 shows $0.39 in Context and
$0.82 against the limit. The Budget block says with sub-agents in its
title when that is what it is showing.
Only the server computes this sum; the sidebar reads the published result. That is deliberate. A sidebar that added up its own number would have to use the TUI state API, which reaches one session at a time and cannot enumerate sub-agent sessions — so it would quietly reproduce Context's figure and show a half-empty bar while the run was being cut off at the limit.
Clone the repo and point both config files at the checkout:
// ~/.config/opencode/opencode.json
{ "plugin": ["/path/to/opencode-budget-limit"] }// ~/.config/opencode/tui.json
{ "plugin": ["/path/to/opencode-budget-limit"] }Relative paths in a config file are resolved against that file's directory.
Then:
npm install
npm run build # src/tui.tsx -> dist/tui.js
npm test # node:test, no test framework to installnpm run build is required before the TUI half will load, and it runs
automatically on prepublishOnly.
The suite covers the failures that would be silent rather than loud: where the limits file resolves to, how older files are migrated, and what total the sidebar is handed. All run on Node 20 and 22, on Linux and macOS, in CI.
Spend publishing is exercised through the plugin's real event hooks against a
stubbed host, using costs taken from an actual session. One case announces only
the root's message, so the child's spend has to arrive through the backfill walk
over session.children() — without it, the suite passes even when sub-agent
cost is dropped entirely and the sidebar falls back to the Context block's
figure.
Path resolution is tested by redirecting HOME and XDG_DATA_HOME and
re-importing src/budget-file.js under a unique query string, because the
legacy path is computed once at import time.
Pushing a v* tag publishes to npm. The workflow runs the tests and the build,
refuses to continue if the tag and package.json disagree, and authenticates
through npm trusted publishing (OIDC) — there is no npm token stored in this
repository.
npm version patch # or minor / major; commits and tags
git push --follow-tagsnpm does not allow a version number to be reused, even after unpublish, so a
tag pushed by mistake burns that number permanently.
opencode compiles Solid JSX on the fly with a Bun plugin, but its filter
deliberately excludes node_modules:
/^(?!.*[/\\]node_modules[/\\]).*\.[cm]?[jt]sx?(?:[?#].*)?$/
So raw .tsx works while you develop from a checkout outside node_modules,
and silently stops working the moment the package is installed. A published
package must ship precompiled JS. build.mjs runs babel-preset-solid with
{ moduleName: "@opentui/solid", generate: "universal" } — the same options the
host uses — so the output targets the host's runtime.
Imports of solid-js and @opentui/solid stay as bare specifiers in the built
output on purpose: opencode rewrites them to its own runtime, and that rewriting
does apply inside node_modules. They are devDependencies, never runtime
dependencies, and this package has no runtime dependencies at all.
src/budget-file.js shared path resolution — imported by both entries
src/server.js enforcement; default-exports { id, server }
src/tui.tsx sidebar block; default-exports { id, tui }
build.mjs src/tui.tsx -> dist/tui.js
test/ node:test suites for path resolution and migration
A plugin module may default-export { id, server } or { id, tui }, but never
both from the same module — opencode rejects that outright. Hence two entries
and two exports keys.
MIT

