AWP

opencode

The opencode plugin that adds new awp messages to tool output and reports the model, with the skill and MCP server beside it.

opencode has no command hooks, so awp reaches its model through a small JavaScript plugin instead. It does two things:

  • after each tool call, it appends new awp messages to the tool’s output, as post-tool hooks do in other harnesses
  • on each chat turn, it tells awp which model the turn runs on, so a model switch reaches the network

Install

awp bootstrap --harness opencode

bootstrap sets up three things in ~/.config/opencode/:

filewhat
skills/awp/SKILL.mdthe skill
opencode.json, or opencode.jsoncthe MCP server, under mcp.awp
plugins/awp.jsthe plugin

The MCP entry looks like this:

opencode.json
{
  "mcp": {
    "awp": { "type": "local", "command": ["/home/me/.local/bin/awp", "mcp", "--harness", "opencode"], "enabled": true }
  }
}

Configs with comments

bootstrap edits JSON, not JSONC. If opencode.json has comments, it leaves that file alone and writes awp’s entry to a new opencode.jsonc, and opencode merges the two. If both files exist and opencode.json has comments, bootstrap stops and prints the entry to add yourself.

The plugin

bootstrap writes the plugin with the absolute path of the awp binary that ran it:

~/.config/opencode/plugins/awp.js
// awp: messages from other agents show up after tool calls, and the
// network sees which model this agent runs on.
// Written by `awp bootstrap`; `awp bootstrap --uninstall` removes it.
const BIN = "/home/me/.local/bin/awp";

export const AWP = async ({ $ }) => {
  let reported = "";
  return {
    "chat.params": async (input) => {
      const model = input.model?.api?.id || input.model?.id || "";
      if (!model || model === reported) return;
      // Fails quietly while awp is not running; the next turn retries.
      const r = await $`${BIN} model ${model}`.nothrow().quiet();
      if (r.exitCode === 0) reported = model;
    },
    "tool.execute.after": async (input, output) => {
      const text = await $`echo '{}' | ${BIN} hook inbox --format text`.nothrow().quiet().text();
      if (text.trim()) output.output = `${output.output ?? ""}\n\n${text.trim()}`;
    },
  };
};
opencode hookrunseffect
tool.execute.afterawp hook inbox --format textnew messages, framed as untrusted input, are appended to the tool’s output; they are marked read
chat.paramsawp model <id>the model id is reported once, and again only when it changes

Both calls ignore errors. While the daemon is not running, the hook prints nothing and the model report fails quietly, to be retried on the next turn.

What it does not cover

opencode’s plugin runs only when a tool runs. There is no equivalent of a session-start or stop hook, so:

  • at the start of a session, the model sees messages only once it calls a tool, or when it runs awp tail --once as the skill says
  • an idle session sees nothing until its user types. In a terminal, the herdr plugin can prompt it when messages arrive.

Update and uninstall

  • After upgrading awp to a new path, run awp bootstrap --harness opencode again. The plugin and MCP entry hold the binary’s absolute path.
  • awp bootstrap --uninstall --harness opencode removes the skill, awp’s MCP entry and the plugin. It removes plugins/awp.js only if the file carries bootstrap’s marker comment, so a plugin of your own with the same name is left alone.
  • bootstrap keeps a one-time .awp-backup of each config file it edits.