npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@preloop-ai/opencode-plugin

v0.2.0

Published

Approve or deny your OpenCode agent's tool calls from your phone, watch, Slack or the web, and steer a running session from anywhere.

Readme

Govern your OpenCode agent: approvals on your phone, spend you can see

Your agent decides to run kubectl delete deployment api. You are not at the keyboard. Right now it just runs.

@preloop-ai/opencode-plugin connects OpenCode to Preloop, the open-source AI agent control plane.

  • Dangerous commands wait for you instead of just running. Native tool calls are gated in tool.execute.before, regardless of what OpenCode's own permission config says, so an opencode.json full of "allow" does not quietly opt you out. The one exception is the safe_read_auto_allow shortcut for read-only shell commands, on by default and documented below.
  • Approve from wherever you actually are. Push to phone or watch, Slack, Mattermost, email, an outbound webhook, or the Preloop console.
  • Nothing runs ungoverned when Preloop is down. The gate fails closed by default; a timed-out approval denies the call.
  • Talk to an agent that is already running. Send a turn into a live OpenCode session or abort it, from the console or the mobile apps.
  • A record of what the agent did, and who let it, with per-agent spend and budget ceilings once the agent is also onboarded to Preloop.

It is Apache-2.0, and it works against either the open-source Preloop control plane you host yourself, or the hosted Preloop Cloud.

The plugin mirrors the contract of the other Preloop runtime plugins (@preloop-ai/openclaw-plugin, @preloop-ai/claude-plugin, preloop-hermes-plugin). Its only runtime dependency is the ws package, used to authenticate the control WebSocket with an Authorization header.

How approvals flow

  1. The plugin reads the preloop.control block that preloop agents onboard OpenCode --approvals writes into ~/.config/opencode/opencode.json (the same command adds the package to the plugin array; OpenCode installs npm plugins with Bun on startup).
  2. It connects to preloop.control.control_ws_url with the durable runtime bearer token (sent as Authorization: Bearer on the WebSocket upgrade via the ws package, so the token never appears in proxy/access-log query strings), advertises presence/capabilities (runtime: "opencode", tool_approval: true), sends heartbeats, and reconnects with backoff.
  3. Every native tool call is gated in tool.execute.before, regardless of OpenCode's own permission config. A user whose opencode.json sets bash, edit, webfetch, ... to "allow" never sees a local prompt and OpenCode never raises permission.asked, so this hook is the only place the call can be intercepted. The plugin maps the OpenCode tool id to the Preloop native tool vocabulary (bash -> Bash, edit -> Edit, write -> Write, read -> Read, glob -> Glob, grep -> Grep, list -> List, webfetch -> WebFetch, task -> Task; MCP tools pass through by name), POSTs {source: "opencode", tool_name, tool_input, session_id, cwd} to Preloop's permission-check endpoint, and awaits the decision. Account tool rules decide first; unmatched calls are escalated to a human approver. On deny (or a fail-closed timeout) the hook throws Preloop denied <tool>: <reason>, which OpenCode reports to the model as the tool error.
    • Read-only tools (read, glob, grep, list) go through only when the backend says so; add an allow rule for them in the Preloop console if you do not want to approve file reads.
    • The one local shortcut is safe_read_auto_allow (default true): a bash command that consists solely of read-only commands (ls, cat, git status, git log, rg, ... and plain | pipelines of them) is allowed without a round trip, including secret files (.env, ~/.ssh/id_rsa, ~/.aws/credentials, and any other path cat/head/ tail can read). The equivalent OpenCode read tool is still gated by Preloop. This mirrors the Preloop CLI hook's Cursor default; set safe_read_auto_allow to false to route those too.
    • Tools served by the Preloop MCP server (preloop_*) are skipped because Preloop already governs them server-side.
  4. For users who keep OpenCode's "ask" permissions, the plugin also receives the permission.asked event through the plugin event hook and replies "once" (approved) or "reject" (denied) through the OpenCode SDK client. Decisions are deduped per tool call (session id + call id): a call the gate already decided is answered from that decision, so the operator is never asked twice for one call.
  5. If no decision arrives within approval_timeout_ms (default 310 s) or the endpoint is unreachable, the configured fallback applies: fail closed (deny, the default) or fail open (approve) via tool_approval_fail_open. There is no local "ask" fallback for the gate: a timed-out approval denies the call.

Operator send_message commands arriving over the control WebSocket are deduped by message_id (the backend replays undelivered commands on reconnect) and delivered into the targeted OpenCode session through the SDK client.session.chat surface.

Remote control

The plugin also steers the local OpenCode agent from the Preloop console, so an operator can drive it remotely like @preloop-ai/openclaw-plugin, @preloop-ai/claude-plugin and preloop-hermes-plugin:

  • Send a turn. A send_message command is forwarded as a user prompt into the targeted OpenCode session. The plugin prefers the async SDK client.session.chat(...) surface and falls back to the documented blocking client.session.prompt({ path, body }) (parts: [{ type: "text", text }]), bounded by turn_timeout_ms (default 300 s) so a hung turn cannot stall the acknowledgement forever. The session itself keeps running.
  • Stop or interrupt. A stop or interrupt command (or a send_message with payload.interrupt: true) maps to client.session.abort({ path }), which aborts the running session.
  • Session targeting. The target session id comes from the envelope (target_session_id, session_reference, runtime_session_id) with a fallback to preloop.control.session_reference. OpenCode sessions are addressed by their native session id; the plugin steers existing sessions and does not create new ones (new_session: false in its presence payload).
  • Status events. Every command emits a command_result frame on success (which the backend accepts as the command ack) or command_error on failure, mirroring the other runtime plugins. Replayed message_ids are acknowledged as completed duplicates without re-executing.

Remote steering can be disabled entirely by setting remote_control_enabled: false; presence then advertises text: false and interrupt: false, and steering commands are rejected while tool approvals keep working. Capabilities are advertised truthfully: text: true only when the SDK client exposes session.chat or session.prompt, and interrupt: true only when session.abort is available.

A note on OpenCode's plugin API

OpenCode documents a typed permission.ask hook, but as of 2026 the permission system never triggers it (see anomalyco/opencode issues #7006 and #9229). The supported surface, used here, is the generic event hook with permission.asked / permission.replied events plus an SDK reply, which is the same integration path OpenCode's own ACP bridge uses.

Install

The plugin is distributed via npm only (there is no OpenCode marketplace listing). Add the package to plugin in your OpenCode config:

{
  "$schema": "https://opencode.ai/config.json",
  "plugin": ["@preloop-ai/opencode-plugin"]
}

Or drop a shim into .opencode/plugins/preloop.ts:

export { PreloopPlugin as default } from "@preloop-ai/opencode-plugin";

Configuration

Written by preloop agents onboard OpenCode --approvals under preloop.control in ~/.config/opencode/opencode.json (the CLI keeps its MCP server entry in the legacy ~/.config/opencode/config.json; OpenCode also reads opencode.json, which is where this block lives):

{
  "preloop": {
    "control": {
      "runtime": "opencode",
      "protocol": "preloop.agent_control.v1",
      "control_ws_url": "wss://example.preloop.ai/api/v1/agents/control/ws",
      "bearer_token": "<durable runtime token>",
      "runtime_principal_id": "<principal id>",
      "permission_check_url": "<optional; overrides the derived permission-check endpoint>",
      "session_reference": "<optional default session>",
      "tool_approval_enabled": true,
      "native_tool_approvals": "on",
      "safe_read_auto_allow": true,
      "tool_approval_fail_open": false,
      "approval_timeout_ms": 310000,
      "remote_control_enabled": true,
      "turn_timeout_ms": 300000
    }
  }
}

Set PRELOOP_OPENCODE_CONTROL_CONFIG to point at an alternative config file.

native_tool_approvals gates the tool.execute.before interception: "off" disables it (the permission.asked bridge keeps working); unset or any other value enables it. preloop agents onboard OpenCode --approvals writes "on" and preloop agents offboard OpenCode removes the block and the plugin entry. safe_read_auto_allow (default true) lets read-only shell commands run without a round trip, including cat/head/tail of secret files (.env, SSH keys, cloud credentials). Set it to false to send those through Preloop too. The OpenCode read tool is not covered by this shortcut and still goes to the backend.

permission_check_url is optional: when unset, the plugin derives <origin>/api/v1/agents/permission-check from control_ws_url. Set it when a proxy or non-standard deployment serves the permission-check endpoint at a different URL.

Manual Test Without Preloop CLI

npm install -g @preloop-ai/opencode-plugin
# add {"plugin": ["@preloop-ai/opencode-plugin"]} to ~/.config/opencode/opencode.json
preloop-opencode-plugin verify --config ~/.config/opencode/opencode.json
preloop-opencode-plugin run --config ~/.config/opencode/opencode.json

Then start opencode, trigger a permission-gated action, and approve/deny it from the Preloop console.

Development

npm install
npm run build   # tsc -> dist/
npm test        # builds, then runs node --test against dist/
npx tsc --noEmit