Give your opencode agent its own Inkbox identity:
a mailbox, a dedicated phone number for SMS/MMS and voice, iMessage,
contacts, notes, and an encrypted credential vault — as native opencode tools.
Email · SMS / MMS · iMessage · Calls · Contacts · Notes · Vault
-
opencode installed and logged in. The plugin loads into real opencode sessions and the gateway drives an
opencode serve— install it (opencode.ai) and authenticate a model provider.inkbox-opencode doctorchecks for it.curl -fsSL https://opencode.ai/install | bash -
Node.js 20+ and git. The installer builds the plugin from source.
-
macOS or Linux. Boot persistence uses a systemd user unit on Linux and a launchd agent on macOS.
-
An Inkbox agent — nothing to set up in advance; the setup wizard self-signs up for you (or takes an existing API key).
This builds the plugin, wires it into your global opencode config, puts
inkbox-opencode on your PATH, and runs the setup wizard:
curl -fsSL https://raw.githubusercontent.com/inkbox-ai/opencode-plugin/main/install.sh | bashThat's the whole setup. The clone lives in ~/.inkbox-opencode/app; the
plugin is installed into your global opencode config (~/.config/opencode)
with a one-file wrapper. The wizard creates a fresh Inkbox agent via
self-signup (or takes an existing API key), enables iMessage, provisions a
dedicated phone number, waits for your START opt-in, validates an OpenAI key
for Realtime voice, mints the webhook signing key, picks the agent's working
directory, and offers to keep the gateway running on every boot. When it
finishes, text, email, or call your agent and it answers.
Re-running is safe — it updates in place (--no-setup installs only;
--start launches the background gateway when done). From a local checkout,
run ./install.sh instead; it uses the checkout in place. Rerun the wizard
anytime with inkbox-opencode setup (setup --print for the static
checklist), and check the wiring with inkbox-opencode doctor.
The plugin is distributed as source — clone and build it, then load it from a local wrapper:
git clone https://github.com/inkbox-ai/opencode-plugin.git ~/.inkbox-opencode/app
cd ~/.inkbox-opencode/app
npm install && npm run build && npm packThen, from your opencode project (or your global ~/.config/opencode
directory), install the packed build and add a one-file plugin wrapper:
npm install ~/.inkbox-opencode/app/inkbox-opencode-plugin-*.tgz
mkdir -p .opencode/pluginsCreate .opencode/plugins/inkbox.ts — opencode auto-loads every file under
.opencode/plugins/. Every option shown in this README goes inside the
InkboxPlugin(input, { ... }) call here:
import InkboxPlugin from "@inkbox/opencode-plugin";
export default async (input: any) => InkboxPlugin(input, {
// options go here — see the examples throughout this README
});Credentials come from environment variables (recommended — keys stay out of committed files):
export INKBOX_API_KEY=sk-ink-... # agent-scoped API key
export INKBOX_IDENTITY=your-agent # agent handleGet both at inkbox.ai/console. They also fall back
to ~/.inkbox/config (the same key = value file the Inkbox CLI and SDKs
read), or pass them as wrapper options:
export default async (input: any) => InkboxPlugin(input, {
apiKey: process.env.INKBOX_API_KEY,
identity: "your-agent",
});Sanity-check the setup by asking the agent to run inkbox_doctor — it reports
config, API reachability, key scope, the resolved identity, and which tool
groups are enabled.
27 tools are enabled by default; 24 more are opt-in (see Enabling more tools). Names are stable — treat renames as breaking.
| Group | Enabled by default | Opt-in |
|---|---|---|
email |
inkbox_send_email, inkbox_list_unread_emails, inkbox_list_emails, inkbox_get_email, inkbox_get_email_thread |
inkbox_forward_email, inkbox_mark_emails_read |
sms |
inkbox_send_sms, inkbox_list_text_conversations, inkbox_get_text_conversation |
inkbox_list_texts, inkbox_get_text, inkbox_mark_text_read, inkbox_mark_text_conversation_read |
imessage |
inkbox_send_imessage, inkbox_list_imessage_conversations, inkbox_get_imessage_conversation |
inkbox_imessage_triage_number, inkbox_list_imessage_assignments, inkbox_send_imessage_reaction, inkbox_mark_imessage_conversation_read |
calls |
inkbox_list_calls, inkbox_list_call_transcripts |
inkbox_place_call |
contacts |
inkbox_lookup_contact, inkbox_get_contact, inkbox_list_contacts, inkbox_create_contact, inkbox_update_contact, inkbox_delete_contact |
— |
notes |
inkbox_list_notes, inkbox_get_note, inkbox_create_note |
inkbox_update_note, inkbox_delete_note |
contact-rules |
inkbox_list_mail_contact_rules, inkbox_list_phone_contact_rules, inkbox_list_imessage_contact_rules (read-only: rule changes are console-only) |
— |
access |
— | inkbox_list_contact_access, inkbox_grant_contact_access, inkbox_revoke_contact_access, inkbox_list_note_access, inkbox_grant_note_access, inkbox_revoke_note_access |
vault |
— | inkbox_credentials_list, and by exact name only: inkbox_credentials_get_login, inkbox_credentials_get_api_key, inkbox_credentials_get_ssh_key, inkbox_totp_code |
diagnostics |
inkbox_whoami, inkbox_doctor |
— |
opencode sends every registered tool's spec to the model on each turn, so the default surface is deliberately lean. Enable more by exact name or by group, then restart opencode:
export default async (input: any) => InkboxPlugin(input, {
tools: { enable: ["inkbox_place_call", "inkbox_forward_email"] },
});tools.enable/tools.disableaccept tool names, group names, or"all". The more specific entry wins (name over group over"all"); at equal specificity, disable wins.- Vault plaintext reads are stricter:
inkbox_credentials_get_login,inkbox_credentials_get_api_key,inkbox_credentials_get_ssh_key, andinkbox_totp_codemust be enabled by exact name —"vault"or"all"never turns them on. Vault tools also need the unlock key inINKBOX_VAULT_KEY(override the variable name with thevault.keyEnvVaroption). inkbox_doctorlists everything that's currently disabled, so the agent can tell you what to enable instead of silently lacking a capability.
Sends and calls are gated before anything leaves:
- Approval prompts (default):
inkbox_send_email,inkbox_send_sms,inkbox_send_imessage,inkbox_forward_email, andinkbox_place_callrequest approval through opencode's native permission system. Approve once, or persist an allow rule from the prompt. - Recipient allowlist: set
outbound.allowedRecipients(exact email addresses / E.164 numbers) and anything else is rejected regardless of approval mode. - Unattended runs: prompts time out after 5 minutes
(
outbound.askTimeoutMs) with a clear error instead of hanging forever. For headless use, either pre-allow the tools in opencode'spermissionconfig, or setoutbound.approvalto"allowlist"(allowlist only — requires a non-emptyallowedRecipients) or"auto".
export default async (input: any) => InkboxPlugin(input, {
outbound: {
approval: "allowlist",
allowedRecipients: ["ceo@example.com", "+15551234567"],
},
});The repo bundles skills that teach the agent Inkbox etiquette — email
triage, SMS/iMessage response patterns, outbound calling, contact and
credential hygiene, troubleshooting. Point opencode at the clone's skills/
directory with one config line:
{
"skills": { "paths": ["$HOME/.inkbox-opencode/app/skills"] }
}(Or copy the skills/ directory into .opencode/skills/ to vendor it. Skills
already installed under .claude/skills/ are picked up by opencode's Claude
Code compatibility loading.)
An Inkbox identity gives the agent:
- A mailbox — a real email address; send, receive, reply with threading.
- A dedicated phone number — SMS/MMS and voice on the same line. New numbers take ~10–15 minutes to propagate to carriers; SMS to US numbers requires the recipient to opt in (text START) per carrier rules.
- iMessage — the agent does not get its own iMessage number. People connect to the agent through the shared Inkbox iMessage line, and each connected person gets a dedicated thread. It's recipient-first by design: no cold outreach; someone messages the agent first, then the agent can reply (and react, and even call) over that connection.
Outbound calls can originate from either line, chosen with origination on
inkbox_place_call:
dedicated_number— the agent's own number, the same line SMS uses. Can call anyone.shared_imessage_number— call a person over the shared iMessage line they already message the agent on. Only works for people connected over iMessage; the underlying number is never surfaced.
When origination is omitted the plugin uses whichever line exists, and
prefers the dedicated number when both do. Note that inkbox_place_call
currently requires an audio bridge: pass clientWebsocketUrl per call or set
the callWebsocketUrl option (env INKBOX_CALL_WEBSOCKET_URL) — Inkbox
connects to that WebSocket for the call's media.
| Option | Env var | Purpose |
|---|---|---|
apiKey |
INKBOX_API_KEY |
Agent-scoped API key (required) |
identity |
INKBOX_IDENTITY (also INKBOX_AGENT_IDENTITY, INKBOX_AGENT_HANDLE) |
Agent handle (required) |
baseUrl |
INKBOX_BASE_URL |
API base URL override |
signingKey |
INKBOX_SIGNING_KEY |
Webhook signature key (future inbound use) |
callWebsocketUrl |
INKBOX_CALL_WEBSOCKET_URL |
Audio-bridge WebSocket for inkbox_place_call |
vault.keyEnvVar |
— (default INKBOX_VAULT_KEY) |
Which env var holds the vault unlock key |
tools.enable / tools.disable |
— | Tool gating (names, groups, "all") |
outbound.approval |
— | "ask" (default) / "allowlist" / "auto" |
outbound.allowedRecipients |
— | Exact-match recipient allowlist |
outbound.askTimeoutMs |
— | Approval prompt timeout (default 300000) |
apiKey, identity, baseUrl, and signingKey resolve in order: plugin
option → env var → ~/.inkbox/config.
Beyond outbound tools, the plugin can run an inbound gateway: a long-lived process that receives email, SMS, iMessage, and phone calls to the agent's identity and turns each into an opencode session that replies on the same channel. It is off by default.
you (phone) ── SMS / iMessage / email / call ──▶ Inkbox ──▶ tunnel ──▶ gateway
│
▼
opencode session
(Inkbox tools, in
your project dir)
Two ways to run it:
-
Sidecar (recommended) — a self-contained companion process:
inkbox-opencode run # or manage it as a daemon: inkbox-opencode start | status | stop
The sidecar needs an opencode server and finds one on its own: an explicit
gateway.serverUrl/OPENCODE_SERVER_URLmust answer; otherwise it attaches to a server already onhttp://127.0.0.1:4096, and failing that it launches its own managedopencode serve(port4097by default; tune withgateway.serve.{bin,port}orINKBOX_OPENCODE_BIN/INKBOX_GATEWAY_SERVE_PORT). A managed server is stopped with the gateway, and its death takes the gateway down so a service manager restarts the pair. -
In-plugin — set
gateway.modeto"plugin"and run insideopencode serveitself. This needs the Inkbox tunnel to work under the host runtime (or a reachablegateway.publicUrl); the sidecar avoids that requirement.
Enable it and point sessions at a working directory:
export default async (input: any) => InkboxPlugin(input, {
gateway: {
enabled: true,
projectDirectory: "/path/to/agent/workspace",
voice: { enabled: true, realtime: { enabled: true } },
},
});The gateway needs a webhook signing key (INKBOX_SIGNING_KEY) to verify
inbound events. What it does:
- Email / SMS / iMessage arrive as sessions keyed per contact (one person, one ongoing conversation across channels); replies go back on the channel the message came in on, threaded for email. Delivery failures wake the agent to retry or switch channels.
- Permission prompts raised inside a gateway session are relayed to the contact on their channel ("reply 1 to allow once, 2 to always allow, 3 to decline") and time out to a decline.
- Control commands (whole-message):
/clear,/stop,/status,/health,/resume,/usage. - Voice (on by default with the gateway): the agent answers calls. Realtime
auto-enables when an OpenAI key is present (
INKBOX_REALTIME_API_KEY, orOPENAI_API_KEYas the backstop) and runs the call as a live raw-audio conversation with in-call actions; otherwise Inkbox handles speech-to-text and text-to-speech. Opt out withINKBOX_VOICE_ENABLED=false(stop answering) orINKBOX_REALTIME_ENABLED=false(force Inkbox STT/TTS).inkbox_place_calldials out with a purpose loaded into the call.
Run inkbox-opencode doctor to check gateway readiness (API reachability,
identity, signing key, opencode server, tunnel/public URL).
Like the claude-code and codex bridges, the gateway can install itself as a boot/login service so the agent stays reachable without a shell:
inkbox-opencode autostart install
inkbox-opencode autostart status | uninstallOn Linux this writes and enables a systemd user unit
(~/.config/systemd/user/inkbox-opencode.service); on macOS, a launchd
agent (~/Library/LaunchAgents/ai.inkbox.opencode.plist). The service runs
inkbox-opencode run from the directory you installed in, launching its own
managed opencode serve — one service is the whole always-on stack.
Install-time details:
INKBOX_*andOPENAI_API_KEYfrom the installing shell are snapshotted to~/.inkbox-opencode/.env(chmod 600) and loaded by the service; real environment variables and~/.inkbox/configstill win/backstop. A./.envin the working directory layers underneath (see.env.example), filling any vars the snapshot doesn't set.- A fork-based
inkbox-opencode startdaemon is stopped first so two gateways never fight over the tunnel. - To keep a Linux service alive while logged out, enable lingering once:
sudo loginctl enable-linger $USER. inkbox-opencode statusreports both the background daemon and the boot service;inkbox-opencode uninstallremoves the service and local state.
One identity gets one tunnel: don't run an always-on gateway on the same identity your CI live suites use, or they will contend for it.
npm install
npm run typecheck # tsc
npm test # vitest (unit + contract)
npm run lint # biome
npm run build # emits dist/
bash scripts/smoke-loader.sh # packs the tarball and loads it through a real opencodeThe contract tests under tests/contract/ pin the opencode plugin API surface
this package depends on; CI runs them against @opencode-ai/plugin@latest
twice daily to catch upstream drift.
Inbound delivery (email/texts arriving as opencode sessions) is being
validated for a future release — see docs/gateway-spike.md for the current
findings.
