Runtime guardrails for OpenCode coding agents. A long session drifts: a new task inherits the last one's "do everything" override, the audit you told it to run never happens, and you find out at 184 tool calls. Token Norm counts the spend and staples the checkpoint onto output the agent is already reading.
npx opencode-token-norm- Cost statement at 25 calls — once per session, before the bulk of the spend.
- Task-boundary detection at 40 — a new user message revokes the stale "do everything" override it would otherwise inherit.
- The audit run for you every 60 calls — read-only, numbers stapled on, no command left for the agent to defer.
- One-call session handoff — persists a structured note, opens a fresh session, pre-fills and submits the prompt.
- Context, cost and token pressure — measured against the real model window; never guessed when it cannot be resolved.
token_norm_status— the same accounting on demand, as machine-readable JSON.
Without Token Norm With Token Norm
task A task A
↓ 184 tool calls ↓ 25 calls ⚠ cost checkpoint
↓ task B inherits ↓ 40 calls ⚠ task boundary: override expired
A's override ↓ 60 calls ⚠ audit, already run
↓ ↓ 🧭 handoff at the pause
3.0M effective tokens task B in a fresh session
An adoption ladder, not a switch. TOKEN_NORM_MODE, default warn:
| Mode | What it does |
|---|---|
observe |
Measures and logs. Injects nothing. |
warn |
Injects the checkpoint into tool output. Default. |
handoff |
Adds a session-split recommendation, but only at a pause. |
block |
Refuses non-cheap tool calls past a hard limit. Opt-in. |
Only block can fail a tool call, and only on a limit you set — pressure alone
never strands a session. Cheap tools and handoff stay open as the exit.
npx opencode-token-normRestart OpenCode. The command copies a self-contained build into
~/.config/opencode/plugins/ and the audit script into
~/.config/opencode/scripts/ — nothing outside ~/.config/opencode is touched.
Two commands make that checkable rather than trusted:
npx opencode-token-norm --dry-run # every path it would write, writes nothing
npx opencode-token-norm doctor # is it installed, current, and configured?doctor exits non-zero only when the plugin is not working; a missing python3
is a warning, because audits are optional and nothing else depends on it. It also
hashes the installed file against the one in the package, which is the only way to
tell a current install from a stale copy. Uninstall with
npx opencode-token-norm uninstall. Requires Node ≥ 22 and an OpenCode build with
plugin support. Requirements, verification, and troubleshooting are in the
install notes.
| Node | OpenCode | @opencode-ai/plugin |
Token Norm |
|---|---|---|---|
| ≥ 22 | V1 plugin API, verified on 1.18.30 | ≥ 1.15.12 | 0.10.x |
The V2 plugin API is not targeted yet. Verified builds, per-release, are in the compatibility notes.
| Capability | Statusline / dashboard | Token rule in AGENTS.md |
token-norm |
|---|---|---|---|
| Who reads it | you, at the edge of the screen | the model, as one more instruction | the model, stapled to output it is already reading |
| When it fires | live, but outside the agent's context | only if the agent chooses to reread it | at 25 / 40 / 60 calls, in-band |
| Audit checkpoint | you run it, or you don't | "run the audit periodically" | already run — read-only, every 60 calls |
| Stale "do everything" overrides | invisible | nothing revokes them | a new task past 40 calls expires them |
| Session split | out-of-band, three manual steps | "split at phase boundaries" | one handoff call, fresh session pre-filled |
| Can block a tool call | no | no | no, unless opt-in block mode |
Soft enforcement, stated plainly. By default the plugin never blocks a tool
call, edits its arguments, or fails one — it changes what the agent can't miss,
not what it can do. Only opt-in block mode refuses non-cheap tool calls while
over budget. A determined agent can still ignore every reminder. The bet is that the
numbers arriving in-band, at the moment of spend, change the plan; if the bet
fails, you still get the honest receipt.
None of this replaces the others: AGENTS.md still defines what "on budget"
means, and a dashboard is still the passive record — this is the runtime layer
between them. It was built against a concrete failure: a token budget written
into AGENTS.md, in context for the whole session, and then 184 tool calls and
3.0M effective fresh tokens spent on a task that should have been three sessions
(call by call).
OpenCode
│
┌───────────┴────────────┐
│ │
session-budget handoff
│ │
tool.execute.after handoff(...)
message.updated │
session.compacting ├─ persist note to disk
│ ├─ open a fresh session
├─ count budgeted calls ├─ pre-fill the prompt
├─ boundary / announce └─ submit it
├─ run the audit
└─ staple a reminder
onto tool output
│
the agent reads it in-band
- Task boundary detection. Past
BOUNDARY_ATcalls (default 40), a new user message revokes any stale "do everything" override, and the next tool call carries the reminder — once per user message, keyed on identity. why - Cost statement at 25 calls. Once per session, the plugin demands the remaining calls, the caps now in effect, and which slice could ship immediately behind a handoff. why 25
- Audit checkpoint every 60 calls. The plugin runs the audit itself — read-only against OpenCode's sqlite DB — and staples the numbers to output the agent is already reading. the metric
- Compaction context. Compaction is the one moment an agent provably re-reads its own rules, so the plugin injects the call count into it.
- The
handofftool. Persists the note before the TUI switch, opens a fresh session, pre-fills and submits the prompt; refused for subagents. design decisions - On-demand status.
token_norm_statusreturns tool calls, context, cost and effective-token usage pluspolicy.current(severity now),policy.peak(severity ever),policy.driver(which axis) and thecontinue | warn | handoff | blockrecommendation, as read-only JSON. The payload is a public interface: stable field names within a major version,peaknever belowcurrent, and reading it never changes it. payload and fields
Two call counts, on purpose. Raw per-session calls drive the behavioral
checkpoints (25 / 40 / 60); weighted session-tree calls drive
TOKEN_NORM_MAX_TOOL_CALLS and budget.toolCalls. So a header reading
calls 37 next to budget.toolCalls: 52 is correct, not a bug — subagents
count toward the tree, and weights scale the budget number only, never the
thresholds.
weighted vs. raw
Process-local by design. Counters live in memory, not on disk: restarting OpenCode resets runtime enforcement state, and the guardrails then undercount rather than re-firing on spend already made. The limits are not durable session policy. why
Full mechanism descriptions, with the exact reminder text and audit output: how it works.
Kept in three separate buckets, because they carry very different weight:
Observed in real usage. On one machine since 2026-09-08: 205 sessions, 227 cost-statement reminders, 69 audits, 583 boundaries, 74 handoffs. After a handoff, 64 of 71 (90%) matched sessions stopped within 5 minutes (median 4 seconds), and none continued past an hour. Single user, no control group.
Benchmark result. n=20 per arm. Plugin load costs 0.158 s → 0.278 s; the wall-time difference is +2.6 s, p = 0.83 — i.e. not distinguishable from noise. An earlier run reported a wall-time effect; that was a harness artifact and the finding was retracted rather than kept.
Hypothesis, not yet demonstrated. That the checkpoints reduce total spend. The behavioral data above is consistent with it and does not establish it — we do not claim the plugin reduced tokens. Method, protocol and limitations: evaluation notes · benchmark.
effective fresh tokens≠ total tokens the model processed. It is cost-weighted input —input + 0.1·cache_read + 1.25·cache_write— which estimates newly-paid-for context, so sessions are comparable across cache hit rates. Do not expect it to match a provider dashboard's token total. the metric
All optional, all environment variables. Three thresholds —
TOKEN_NORM_ANNOUNCE_AT, TOKEN_NORM_AUDIT_EVERY, TOKEN_NORM_BOUNDARY_AT —
plus two kill switches: TOKEN_NORM_BUDGET=0 disables the counting half, and
TOKEN_NORM_HANDOFF=0 drops the handoff tool. The guardrails — thresholds
and mode — are on by default; the measured budgets (TOKEN_NORM_MAX_COST,
TOKEN_NORM_MAX_EFFECTIVE_TOKENS, TOKEN_NORM_MAX_TOOL_CALLS,
TOKEN_NORM_CONTEXT_WARN, TOKEN_NORM_CONTEXT_LIMIT) are opt-in, and unset
means unenforced. All options and defaults:
configuration.
The audit script ships inside the package and reads the same DB the plugin does:
python3 node_modules/opencode-token-norm/scripts/usage-audit.py --lastModes --session, --top and --receipt (each accepts --json), the
paste-ready receipt, and the effective fresh tokens formula are in the
audit notes.
This plugin replaces none of your rules. It makes three of them unskippable and leaves the rest to you — read windows, smallest test target, subagent delegation, output caps. A counter cannot know any of those. The reminders are written to point back at the file that does.
- Design notes — why the thresholds are where they are, the task-boundary model, the handoff decisions, session-state and safety boundaries, and what the effective-fresh number actually measures.
- Advice vs. enforcement — the rule-by-rule case for why a norm sitting in context is not a norm, and the answer to "isn't this just prompt engineering?"
- Post-mortem — the session that audited itself, reported 3.0M tokens of waste, and kept going anyway. Every threshold here traces back to a specific moment in it.
- Source · Issues · npm
- Changelog · Contributing · Security
- If this plugin saved you tokens, a star helps others find it.
MIT
