RimZ
ReferenceAgent plugins

Agent plugins

An agent plugin connects an agent CLI that RimZ has no built-in adapter for. It is a bundle directory with three parts: a manifest that declares the agent's identity…

An agent plugin connects an agent CLI that RimZ has no built-in adapter for. It is a bundle directory with three parts: a manifest that declares the agent's identity, launch flags, and what it reports; a shim the agent's own hooks call, which translates native events into RimZ's canonical JSON envelope; and optional probe executables RimZ runs to read spend, account, and version.

Early surface, not ready for public use. The bundle format, the canonical envelope, and the probe contracts are under active development and change without notice. Prototype against them and send feedback, but do not depend on them in production.

A plugin is machine configuration: RimZ runs the launch, resume, and probe commands its manifest declares. Install only bundles you trust. Project configuration cannot register a plugin, so plugin commands sit outside project trust.

Register a bundle

rimz agents register <kind> creates a working skeleton under ~/.rimz/agents.d/<kind>/ ($RIMZ_HOME/agents.d/<kind>/ when RIMZ_HOME is set):

$ rimz agents register mybot
registered `mybot` at /home/me/.rimz/agents.d/mybot
edit agent.toml and README.md, then run `rimz agents register --check`
FileStarts as
agent.tomlA valid manifest that emits only session_start, with the launch flags, [transcripts], and [probes] commented out.
README.mdThe setup document rimz doctor points at. Write the agent's hook installation steps here.
shim.shThe forwarding shim from Shim contract, executable.
probes/spendA stub spend probe that returns no entries, executable.
probes/accountA stub account probe that reports logged out, executable.

The scaffold's [probes] entries are commented out, so the stub probes do nothing until you uncomment them; until then rimz agents check prints probes: none declared.

register refuses a kind that breaks the kind rules, a built-in kind such as claude, a kind a valid bundle already uses, and a directory that already exists.

rimz agents register --check validates every bundle under agents.d and creates nothing. It prints N agent plugin(s) valid, or exits 1 listing each invalid manifest with its path and error.

The complete ScriptBot example adds a fake agent, a shim, a priced spend probe, an account probe, and a fixture transcript.

Check a plugin

rimz agents check <kind> is the authoring loop: run it after each change to the manifest, shim, or probes.

rimz agents check mybot
rimz agents check mybot --spend-file ~/.mybot/sessions/sess-123.jsonl
rimz agents check mybot --replay ./events.jsonl
FlagEffect
--spend-file <PATH>Runs the spend probe on this transcript. Without it, the spend probe is reported skipped.
--replay <JSONL>Parses each line as one envelope and steps it through RimZ's agent state machine, without touching any room.

The report names the manifest, counts the coverage the manifest derives, and runs each declared probe. The account and version probes always run. This run used the manifest from Manifest; the stderr warning lines for undeclared events are left out:

$ rimz agents check mybot --spend-file sess.jsonl --replay events.jsonl
plugin `mybot`
manifest: valid (/home/me/.rimz/agents.d/mybot/agent.toml)
coverage: 5 wired, 1 partial, 12 unsupported
lifecycle: 4 native, 1 derived, 6 absent
probes:
  spend: ok (present, executable; ./probes/spend) — 0 entries
  account: ok (present, executable; ./probes/account) — canonical account response
  version: ok (present, executable; mybot --version) — mybot 1.4.2
replay: events.jsonl
LINE  EVENT           SIGNAL          STATE              RESULT
1     session_start   registered      idle               ok
2     turn_start      turn_started    running/reasoning  ok
3     tool_use        tool_used       running/acting     ok
4     awaiting_input  awaiting_input  waiting            warning: event `awaiting_input` is absent from manifest emits
5     turn_end        rejected        unchanged          error: invalid canonical envelope: invalid type: string "yes", expected a boolean
6     turn_end        turn_ended      success            ok
7     future_event    unknown         unchanged          warning: event `future_event` is absent from manifest emits
8     turn_start      rejected        unchanged          error: lifecycle observation has no agent identity
final AgentState:
  sess-123: status=success, phase=idle, compacting=false
error: agent plugin check failed

Each probe line is ok, skipped, or failed, followed by whether the executable exists and has its executable bit, the command, and a detail: the entry count, the version string, or the failure reason.

A replay row's SIGNAL is the lifecycle signal the envelope produced, rejected when RimZ would drop it, or unknown for an event name outside the vocabulary. STATE is the agent's state after the row. A row is rejected for invalid JSON, a missing hook_event_name, a field of the wrong type, a protocol other than 1, a missing session_id, or an agent_id that fails subagent identity. An event missing from emits is accepted with a warning, as live ingestion accepts it.

Replay differs from a live room in two places. A live room drops the rejected envelopes without an error. And a live room records no waiting state for awaiting_input when the manifest sets native-ask-ui = false, while replay still shows waiting (capabilities).

check exits 1 when any declared probe is missing, is not executable, or fails, when any replay row is rejected, or when the --spend-file or --replay file cannot be read. check --spend-file also fails a spend entry whose timestamp is not RFC 3339, which a live room skips. check refuses a built-in kind, a kind with no bundle, and an invalid manifest, printing the manifest error.

Bundle layout

~/.rimz/agents.d/mybot/
├── agent.toml        manifest
├── README.md         setup-doc: how to install the agent's hooks
├── shim.sh           called by the agent's hooks
└── probes/
    ├── spend
    └── account

RimZ reads every agents.d/*/agent.toml once per process. The directory name must equal the manifest's kind.

These plugin manifests remain TOML. They register provider kinds and are separate from the Markdown definitions that select models, tools, crafts, and team seats.

RimZ installs no hooks for a plugin: rimz hooks install has no installer for an unknown agent, so wiring the agent's native hooks to the shim is the bundle's job, documented in its setup-doc.

Manifest

A manifest using every field that has an effect:

protocol = 1
kind = "mybot"
display-name = "MyBot"
process-names = ["mybot", "node"]
emits = ["session_start", "turn_start", "turn_end", "tool_use", "context"]
setup-doc = "README.md"

[brand]
emblem = "[mb]"
color = 141
color-rgb = [175, 135, 255]

[capabilities]
native-ask-ui = true
subagents = false
registers-lazily = false
context-usage = true

[tools]
mutating = ["write", "shell"]
editing = ["write"]

[launch]
bin = "mybot"
args = []
model-flag = "--model"
effort-flag = "--effort"
system-prompt-file-flag = "--system-prompt-file"
resume = ["mybot", "--resume", "{session_id}"]
compact-command = "/compact"

[launch.permission-args]
ask = []
auto = ["--auto"]
yolo = ["--yolo"]
plan = ["--plan"]

[transcripts]
globs = ["~/.mybot/sessions/*.jsonl"]
thread-key = "per-file"

[probes]
spend = ["./probes/spend"]
account = ["./probes/account"]
version = ["mybot", "--version"]

Keys are kebab-case. RimZ ignores keys it does not recognise in every table, so a misspelled key such as native_ask_ui has no effect and raises no error; rimz agents check shows the coverage the manifest actually derives.

Top-level keys

KeyRequiredMeaning
protocolyesMust be 1.
kindyesThe agent's name in rimz agents <kind>, profiles, and addresses. Lowercase letters, digits, and -, starting and ending with a letter or digit. Must equal the bundle directory name and differ from every built-in kind and every other plugin.
display-nameyesName shown in the sidebar and reports. Must not be empty.
process-namesyesProcess names RimZ matches to find the agent running in a pane. At least one, no duplicates.
emitsyesThe canonical events the shim sends, from Events. Must include session_start, which creates the session. No duplicates or unknown names.
setup-docyesPath to the hook installation guide, relative to the bundle. The file must exist.

[brand]

KeyDefaultMeaning
emblemthe shared fallback emblemShort text shown as the agent's emblem.
color141Terminal 256-color index.
color-rgb[175, 135, 255]Truecolor value.

[capabilities]

Every key defaults to false.

KeyMeaning
native-ask-uiThe agent shows its own permission, plan, and question prompts. RimZ records awaiting_input as a waiting agent only when this is true; when false, those events change nothing.
subagentsThe agent runs child agents. The subagent coverage claim needs this plus subagent_start and subagent_end in emits.
registers-lazilyThe agent can be running before its first session_start arrives, so RimZ binds a late session to its pane by working directory.
context-usageClaims a context gauge in coverage. Emitting context claims it too.
background-tasksAccepted and has no effect.

[tools]

KeyMeaning
mutatingtool_name values that change files or state. A tool_use naming one, with is_error false, counts as a mutation.
editingtool_name values that edit files. Each must also appear in mutating.

[launch]

Without [launch], RimZ has no command to start the agent. With it, rimz agents mybot "fix the parser" runs bin, then args, then the mode, model, effort, and prompt-file flags and any profile args, then -- and the prompt.

KeyMeaning
binExecutable. Must not be empty.
argsArguments always passed after bin.
model-flagFlag that takes the model value, as in --model mybot-pro.
effort-flagFlag that takes the effort value.
system-prompt-file-flagFlag that takes the path of the composed system prompt file and replaces the agent's system prompt. Declare it only for a flag that reads a file and replaces the whole prompt.
resumeFull argv to resume a session. Must contain {session_id}, which RimZ replaces with the session id.
compact-commandText RimZ sends into the pane to compact the agent (rimz agents compact and smart compaction). Must not be empty. RimZ sends it bare: rimz agents compact refuses an instruction for a plugin, and refuses the plugin entirely unless emits includes turn_start.

A launch that sets model, effort, or system-prompt-file fails before any pane opens when the matching flag is not declared. A launch that sets auto-compact always fails for a plugin, since the manifest has no field for it (auto-compaction window).

[launch.permission-args] holds the arguments each permission mode (ask, auto, yolo, plan) appends. Each defaults to no arguments, so the agent keeps its own default. Modes are described in permission modes.

[transcripts]

KeyDefaultMeaning
globsnoneTranscript file patterns. ~/ expands to $HOME, an absolute pattern stands as written, and a relative pattern resolves from the bundle directory.
thread-keyper-fileper-file counts each transcript file as one session; session-dir counts each directory as one.

Spend history needs both [transcripts] and a spend probe: RimZ sends each matching file to the probe.

[probes]

Each probe is an argv array: spend, account, and version. See Probe contracts.

Paths

An executable in bin, resume, or a probe resolves the same way. An absolute path is used as written, a path containing / (such as ./probes/spend) resolves from the bundle directory, and a bare name (such as mybot) is looked up on PATH.

Canonical event envelope

The shim sends one JSON object per event. Three fields are required:

FieldMeaning
protocol1. Any other value is dropped.
hook_event_nameOne of the events.
session_idThe root session's id. An event without one updates no session.

Subagent identity

A child agent's events carry the parent's id in session_id and the child's own id in agent_id. A subagent_start or subagent_end whose agent_id is missing, blank, or equal to session_id is dropped. Any other event whose agent_id differs from its session_id is treated as the child's and dropped, so it never advances the parent.

Shared fields

Any event may carry these optional fields:

FieldTypeMeaning
agent_idstringChild id, see Subagent identity.
cwdstringWorking directory; RimZ uses it to match the agent to a worktree.
modelstringModel in use.
effortstringEffort level in use.
context_pctintegerPercent of the context window used; values above 100 read as 100.
context_windowintegerContext window size in tokens.
total_tokensintegerTokens in context.
input_tokens, output_tokensintegerToken counts for the latest turn.
cache_read_input_tokens, cache_write_input_tokensintegerCache token counts.
transcript_pathstringThe session's transcript file.
total_cost_usdnumberSession cost in US dollars. Read from context events only.
rate_limitsobject{"windows": [...]} in the window shape. Read from context events only.

Events

EventEvent fieldsEffect
session_startnoneRegisters the session.
turn_startprompt?Starts a turn; prompt becomes the turn's task.
turn_enderrored?, error_message?, last_assistant_message?Ends the turn, as failed when errored is true. last_assistant_message becomes a supervised run's output.
tool_usetool_name?, is_error?Records tool activity, classified by [tools].
awaiting_inputask (permission, plan_approval, or question), question?Marks the agent waiting, when native-ask-ui = true.
compaction_startnoneMarks the agent compacting.
compaction_endtrigger? (auto or manual)Ends compaction.
subagent_startchild agent_idRegisters a child under the parent session_id.
subagent_endchild agent_id, errored?Ends the child as succeeded or failed.
session_endnoneEnds the session and hides its card; the session stays available to resume.
contextshared fields, plus the context fields belowUpdates the card's context, cost, and rate limits without changing state.

A context event also accepts the optional fields of RimZ's AgentContext record, including session_name, session_preview, model_id, effort, model_display_name, thinking_enabled, output_style, vim_mode, agent_version, cost, tokens, rate_limits, pr, and account. The top-level model, token, context_pct, total_cost_usd, and rate_limits fields fill the same record when its own fields are absent. RimZ sets source and observed_at itself.

Dropped envelopes

RimZ drops an envelope without an error, changing nothing, when a field has the wrong type (such as "errored": "yes"), awaiting_input has no valid ask, protocol is not 1, or hook_event_name differs from the shim's --event. An event name outside the vocabulary is also ignored. An event that is valid but missing from emits is processed, and RimZ logs a warning once per event; add it to emits so coverage stays accurate. rimz agents check --replay shows each of these decisions.

Shim contract

The agent's hook calls the shim with the event name as its argument and the envelope on stdin. The scaffold's shim forwards an envelope that is already canonical:

#!/bin/sh
set -eu
event=${1:?canonical event name}
exec rimz hooks feed --source mybot --event "$event"

--source is the plugin's kind. --event is optional; without it, RimZ reads the event from hook_event_name, and when both are given they must match.

rimz hooks feed prints nothing on stdout for a plugin event, and a dropped envelope still exits 0. Stdin that is not JSON exits 1. RimZ never answers a prompt on the agent's behalf: the user answers in the agent's own UI. Before setting native-ask-ui = true, confirm that an empty, successful hook response leaves the agent's prompt in place.

Probe contracts

Probes are optional enrichment. RimZ runs each one in the bundle directory, with stdin, stdout, and stderr piped:

LimitValue
TimeKilled after 3 seconds.
stdoutAt most 1 MiB.
stderrThe first 16 KiB, quoted in the warning when the probe exits nonzero.
FailureA missing executable, nonzero exit, timeout, oversized output, or invalid JSON drops that reading and logs a warning. Session state is never affected.

Spend

RimZ sends one JSON line per transcript file matched by [transcripts]:

{"file":"/home/me/.mybot/sessions/sess-123.jsonl","cursor":{"line":18}}

cursor is null on the first request for a file, and afterwards whatever the probe last returned. Return the entries after that cursor and a new cursor:

{
  "entries": [
    {
      "thread_id": "sess-123",
      "timestamp": "2026-06-01T12:00:00Z",
      "model": "mybot-pro",
      "input_tokens": 1200,
      "output_tokens": 300,
      "cache_read": 400,
      "cache_write": 100,
      "cost_usd": 0.02
    }
  ],
  "cursor": {"line":19},
  "origin": "/work/project"
}
FieldMeaning
entries[].timestampRequired, RFC 3339. An entry with an unparseable timestamp is skipped.
entries[].cost_usdCost in US dollars, used as given; RimZ applies no price table. Missing counts as 0.
entries[].thread_id, model, input_tokens, output_tokens, cache_read, cache_writeOptional. Token counts still appear in rimz stats when cost is 0.
cursorAny JSON value; RimZ returns it unchanged on the next request.
originOptional working directory the transcript belongs to.

Account

The account probe gets empty stdin and prints one JSON object:

{"plan":"pro","account_id":"account-123","rate_limit_windows":[{"used_percentage":42,"duration_mins":300,"resets_at":"2026-06-01T17:00:00Z","source":"authoritative"}]}
FieldMeaning
planPlan name shown on the provider label.
account_idIdentity label for the account.
rate_limit_windowsUsage windows for the provider dashboard.
logged_outtrue reports the user logged out. A response with none of plan, account_id, and rate_limit_windows counts as logged out too.

Each window uses the fields listed in rimz providers window fields: used_percentage (an integer from 0 to 100), resets_at, and either duration_mins for a time window or scope for a named quota. A scope needs both id and label. Set source to "authoritative" when the reading comes from the provider's usage API; without it the reading counts as best-effort. A window of the wrong shape, such as a fractional used_percentage, invalidates the whole response. A context event from a live session may send the same windows in rate_limits.

A named quota with no fixed length carries a scope with a stable id and a short label (three cells fit the dashboard slot) and no duration_mins:

{"plan":"business","account_id":"account-123","rate_limit_windows":[{"scope":{"id":"build_minutes","label":"bld"},"used_percentage":42,"resets_at":"2026-06-01T17:00:00Z","source":"authoritative"}]}

Without duration_mins, the dashboard shows the reset time but no refill, burn pace, or not-started state. A window with both scope and duration_mins is a sub-cap: it draws as a tick on the unscoped window of the same duration, and not at all when that window is absent. The provider dashboard shows how bars and ticks read.

{"logged_out":true}

Version

The version probe gets empty stdin. Its first non-empty stdout line is the agent version.

Coverage and doctor

rimz coverage lists every valid plugin after the built-in agents, with claims derived from the manifest. Run rimz agents check for the counts; the agent support page defines what each claim means.

ClaimWired when
Turn lifecycleemits has session_start, turn_start, and turn_end
Permission, plan approval, questionemits has awaiting_input and native-ask-ui = true
Compactionemits has compaction_start and compaction_end
Subagentssubagents = true and emits has subagent_start and subagent_end
Session endemits has session_end
Context usagecontext-usage = true, or emits has context
Realtime cost, rich contextemits has context
Account spenda spend probe is declared
Idle notificationalways partial, derived from turn_end
Answering prompts, hook install, remote control, tool statistics, launch reminders, background parkingnever

rimz doctor prints an AGENT PLUGINS section when any bundle exists, one row per manifest:

StatusDetail
validThe setup document's path.
valid; probe unavailablecheck <probes>, naming each declared probe that is missing or not executable.
invalidThe manifest error.

An invalid bundle anywhere under agents.d is a failed precondition for a room: rimz start refuses before creating anything, lists every manifest error, and names rimz agents register --check. Elsewhere the broken kind is absent. rimz hooks feed --source <kind> for it exits 0 with no output when stdin is JSON, so the agent's hooks keep working while you fix the manifest.

Protocol version

protocol = 1 in the manifest and "protocol": 1 in each envelope name the only version RimZ reads. A manifest declaring another version is invalid, and an envelope carrying another version is dropped.

See also

On this page