-
Notifications
You must be signed in to change notification settings - Fork 0
Bizar Plugin
The Bizar plugin is an opencode plugin bundled with BizarHarness. It runs inside opencode alongside the agents and does three things: detects subagent loops, reports per-session activity, and injects handoff messages so a stuck subagent can be reassigned. It is the only piece of BizarHarness that runs as a plugin (not as an agent).
- Loop detection. Fingerprints every tool call and counts how often the same fingerprint appears in the recent window. When the count crosses a threshold, the plugin acts: warn at 5, escalate at 8, hard-block at 12.
-
Periodic status reporting. Logs every tool call (metadata only) to
~/.cache/bizarharness/logs/<sessionId>.log. The log is one line per call and contains no tool args, paths, or session content. -
Handoff signal. When a subagent is clearly stuck, the plugin injects a system message into the agent's next-turn context. The message is a static string that tells the subagent to use the
tasktool to escalate to its parent.
The plugin is read-only on the project. It makes no outbound network calls and writes only to ~/.cache/bizarharness/.
The plugin fingerprints each tool.execute.before call as a stable hash of the tool name and the normalized arguments. It keeps a rolling window of the last 10 (default) tool calls per session. When the count of matching fingerprints in the window crosses a threshold, the plugin acts:
| Repetitions in last 10 | Action | Mechanism |
|---|---|---|
| 3 | Log a warning via client.app.log. No injection. |
Diagnostic only |
| 5 (warn) | Inject system message via experimental.chat.system.transform
|
Subagent sees it on its next turn |
| 8 (escalate) | Inject stronger system message | Subagent sees it on its next turn |
| 12 (block) |
Block. Throw from tool.execute.before
|
Surfaces in the TUI as a tool error |
The plugin's hard block at threshold 12 runs before opencode's built-in doom_loop recovery. The plugin wins.
Per-session log files are written to:
~/.cache/bizarharness/logs/<sessionId>.log
The log rotates at 10 MB by default. Rotation keeps the last 3 files: plan.html → .1.log → .2.log → .3.log. The .3.log is deleted.
The per-call log line is metadata only:
2026-06-17T14:30:01.123Z session=<sid> tool=read fingerprint=ab12cd outcome=ok duration=45ms
It contains the ISO timestamp, session ID, tool name, fingerprint hash, outcome, and duration. It does not contain raw tool args, session content, environment values, or LLM output. This is the §7.6 invariant from the plugin spec and is verified by the integration test.
State files (per-session metadata) are written to:
~/.cache/bizarharness/<sessionId>.json
State is keyed by session ID, not by agent name. The parentAgent field is seeded from the first user message in a session and is not updated for subagent dispatches within the same session.
The plugin injects one of three static message templates depending on the threshold crossed. The strings are literal — do not modify in the agent prompts:
| Threshold | Emitted string |
|---|---|
| 5 | [loop guard: 5 identical calls to <tool>]. Consider using the task tool to report back to your parent with what you've learned and what you need. |
| 8 | [loop guard: 8 identical calls to <tool>]. Consider using the task tool to report back to your parent with what you've learned and what you need. |
| 12 (throw) | Loop protection: 12 identical calls to <tool>. Use task to escalate. |
The only interpolation is <tool>, which is the tool name from opencode's tool registry (e.g., read, bash, edit). It is not user-controlled content.
Odin matches on these literal substrings. Every BizarHarness subagent prompt includes a ## Loop Guard Handling section that tells the agent to recognize these strings and use the task tool to escalate. The section is byte-identical across all twelve subagents.
Plugin options are passed in the opencode.json plugin array:
| Option | Default | Notes |
|---|---|---|
loopThresholdWarn |
5 | Clamped to Math.max(1, floor(value))
|
loopThresholdEscalate |
8 | Auto-set to warn + 1 if out of order |
loopThresholdBlock |
12 | Auto-set to escalate + 1 if out of order |
loopWindowSize |
10 | Clamped to [3, 50]
|
logDir |
~/.cache/bizarharness/logs |
Refused if inside ~/.ssh/, ~/.gnupg/, ~/.aws/, ~/.kube/
|
stateDir |
~/.cache/bizarharness |
Same secret-dir refusal |
logRotationBytes |
10485760 (10 MB) | Math.max(1024, floor(value)) |
Missing options fall back to defaults. Bad input is clamped, never rejected. The plugin never throws on bad config.
| Env var | Effect |
|---|---|
BIZAR_DISABLE=1 |
Disables the plugin entirely. Returns empty hooks; logs once at debug level. |
BIZAR_DISABLE_LOOP=1 |
Loop guard disabled. Status reporting still active. |
BIZAR_DISABLE_LOG=1 |
Status reporting disabled. Loop guard still active. |
BIZAR_LOG_LEVEL=debug|info|warn|error |
Log verbosity. Default info. Invalid values fall back to info. |
Env vars are read once at plugin init. Mid-session changes are ignored.
These are documented in the plugin spec and are part of the release contract. Custom integrations must work around them, not against them:
-
Syntactically different but semantically identical args are not caught.
ls -laandls -la .produce different fingerprints. -
Cross-tool loops are not caught. A
read→grep→read→greppattern is not detected (the fingerprint includes the tool name). -
Arg-mutating loops are not caught.
read foo1,read foo2,read foo3produces three distinct fingerprints even if the intent is to loop. -
Custom agents without the
## Loop Guard Handlingsection will loop indefinitely past threshold 12. The plugin throws at threshold 12, but a subagent that doesn't recognize the message and usetaskwill simply retry. All BizarHarness subagents include the canonical section. Users who add custom agents without this section will experience infinite-block loops. This warning is mandatory. See Limitations for the full list. - Corrupt state files are not auto-recovered. A corrupt JSON file is logged and ignored; the session starts with empty state. The corrupt file is preserved for forensic inspection.
- Out-of-worktree paths are hashed, not stored. A loop involving files outside the worktree produces stable fingerprints across runs (good) but the original path is not recoverable from the log.
-
Stale session cleanup is best-effort. If
client.session.list()fails, the age-based cleanup still runs but the "session no longer in opencode" branch is skipped. -
Single-host state. State files are local to
~/.cache/bizarharness/. Cross-host loop detection is out of scope. - Env var changes mid-session are ignored. Env vars are read once at plugin init.
-
Log rotation is best-effort. If a
renameSyncfails, that step is skipped and a warning is logged. The log may grow pastlogRotationBytesin degenerate cases. -
Canonical handoff messages hardcode the default threshold numbers. The warn, escalate, and block message templates contain the literal text
"5 identical calls","8 identical calls", and"12 identical calls". If you reconfigure the thresholds via plugin options, the action still fires at the new counts, but the message text still says the defaults. The agent prompts' recognition patterns match the default text — non-default thresholds may cause subagents to fail to recognize the handoff. Leave the thresholds at defaults unless you also update the agent prompts.
To disable the plugin for a single session, set BIZAR_DISABLE=1 in the environment before launching opencode:
BIZAR_DISABLE=1 opencodeTo disable only the loop guard (status reporting still active):
BIZAR_DISABLE_LOOP=1 opencodeTo disable only status reporting (loop guard still active):
BIZAR_DISABLE_LOG=1 opencodeTo disable the plugin permanently, remove the entry from the plugin array in opencode.json and remove the plugins/bizar/ directory from ~/.config/opencode/.
The plugin is verified to:
- Not import
node:dns,node:net,node:http, ornode:https. - Not call any external API.
- Not write outside
~/.cache/bizarharness/(configurable). - Not read environment variables other than the four documented above.
- Not override agent prompts (only injects ephemeral system messages into the current turn's context).
- Not modify user files.
The forbidden-import check is enforced by scripts/check-forbidden-imports.sh in the plugin repo and runs as part of the test script. CI fails the build if any forbidden node: import is found.
Next: Background Agents — the experimental async subagent system that runs on top of the Bizar plugin (v0.4+).
Norse-pantheon multi-agent system for opencode.