Skip to content

About

Give your Claude Code agent its own Inkbox identity: a mailbox, iMessage, a phone number for calls and SMS, and an internet address. Talk to it from anywhere.

Topics

Resources

Stars

19 stars

Watchers

0 watching

Forks

Repository files navigation

Claude Code Inkbox Bridge

Claude Code, now with a phone



Give your Claude Code agent its own Inkbox identity:
a mailbox, iMessage, a phone number for calls and SMS, and an internet address.
Step away from the keyboard and keep working with it from anywhere.

Slack · Email · Calls · SMS / MMS · iMessage · Tunnel



Prerequisites

  • Claude Code installed and logged in. The bridge drives a real Claude Code session, so the claude CLI has to be on the machine and authenticated — install it (claude.com/claude-code), then either sign in with a Claude Pro/Max subscription or set ANTHROPIC_API_KEY. inkbox-claude doctor checks for it.
  • Python 3.11+. The installer finds one and builds the bridge its own venv.
  • 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).

Get started — one command

This finds a Python 3.11+, installs the bridge in its own venv, puts inkbox-claude on your PATH, and runs the setup wizard:

curl -fsSL https://raw.githubusercontent.com/inkbox-ai/claude-code-plugin/main/install.sh | bash

That's the whole setup. The wizard creates a fresh Inkbox agent for you (or takes an existing API key), connects iMessage, provisions a dedicated phone number, mints a webhook signing key, picks the project directory Claude works in, and offers to keep the bridge running on every boot. When it finishes, text/email/call your agent and it answers from a real Claude Code session.

The one thing to have ready: be logged into Claude — a Claude Pro/Max subscription (via the Claude Code app/CLI) or ANTHROPIC_API_KEY set. The installer checks this and warns if it's missing.

Flags: --start (launch the background gateway when done), --no-setup (install only). From a local checkout, run ./install.sh. Re-running is safe.

Bootstrap an existing identity without prompts

For unattended agent setup, keep the identity handle, base URL, and API key supplied by the human who assigned the identity. Do not create a replacement identity. First download the installer once, inspect the exact file that will run, execute that same file with setup disabled, and remove it even if a step fails:

(
  set -euo pipefail
  installer="$(mktemp)"
  trap 'rm -f "$installer"' EXIT
  curl -fsSL https://raw.githubusercontent.com/inkbox-ai/claude-code-plugin/main/install.sh -o "$installer"
  cat "$installer"
  bash "$installer" --no-setup
)

Stop if the downloaded script contains anything unexpected. Once the human-provided credential is available as the transient INKBOX_API_KEY environment variable, run the non-interactive bootstrap:

inkbox-claude bootstrap --identity '<handle>' --base-url '<url>' --project-dir "$PWD" \
  --voice-ai --rotate-signing-key --start-gateway
unset INKBOX_API_KEY

Keep the API key out of command-line arguments, source control, project instructions, and other persistent text. Supply it through a private channel, expose it only to the bootstrap process, and unset the transient environment variable afterward. Bootstrap stores the resulting agent-scoped configuration in the bridge's private local configuration.

bootstrap validates that the key can access the requested existing identity, scopes down an admin key before saving it, preserves existing Voice AI settings, and starts or restarts the detached gateway. Signing-key replacement is explicit because it transfers verified webhook delivery away from any gateway using the previous key. The command prints a secret-redacted JSON result and is safe to resume.

  • configured: continue to verification.
  • requires_human: show the human every entry in human_actions, wait for them to complete the requested action, then rerun the exact same bootstrap command. Keep using the assigned handle; do not create another identity.
  • error: use the JSON error and inkbox-claude doctor output to diagnose the failed prerequisite or configuration, correct it, then rerun the exact same bootstrap command. Partial progress is preserved across retries.

Check it any time:

inkbox-claude doctor    # config, SDKs, claude CLI, identity reachability
inkbox-claude status    # is the background gateway up? where are the logs?

What it does

you (phone)  ── SMS / iMessage / email / call ──▶  Inkbox  ──▶  tunnel  ──▶  bridge
                                                                              │
                                                                              ▼
                                                                  Claude Code session
                                                                  (full tool access in
                                                                   your project dir)
  • Text, iMessage, email, or call your agent's Inkbox number. Each remote party gets one Claude Code session spanning every channel — text it on the walk home, then email it details, same conversation.

  • Claude Code runs with full tool access in CLAUDE_PROJECT_DIR. It reads, searches, and browses freely; anything risky (running commands, editing files) is escalated to you as a text:

    Claude wants to run the command: npm test

    Reply 1 (or YES) to allow once, 2 (or ALWAYS) to allow this kind of action for the rest of the session, 3 (or NO) to block it.

  • When Claude needs you to pick between options (the AskUserQuestion tool), you get a numbered poll on whatever channel you're on, and your reply is fed back as the answer.

  • Each message you send is tagged with its channel, so Claude knows whether it's on SMS, iMessage, email, or a call.

  • A channel prompt is appended to Claude Code's system prompt so replies fit a phone: plain text, no markdown, short, jargon kept to a minimum ("saved and published the change", not "pushed to origin/main").

  • Claude also gets Inkbox tools (inkbox_send_email, inkbox_send_sms, inkbox_send_imessage, …) so it can proactively reach you — "email me the full report" works.

Manual install

If you'd rather not run the installer (any Python 3.11+ environment):

pip install -e .

inkbox-claude setup    # interactive wizard — writes .env for you
set -a; source .env; set +a

inkbox-claude doctor
inkbox-claude run

inkbox-claude setup walks you through everything and writes .env: create a fresh Inkbox agent via self-signup (or bring an existing API key), pick or create the identity, attach the Claude Code avatar to the agent's contact card (auto for a new self-signup agent; offered for an existing one with no avatar), provision a phone number, wait for your START opt-in, optionally enable OpenAI Realtime voice (validating your key), connect iMessage, mint a webhook signing key, choose the project directory, and set up autostart. Rerun it anytime to reconfigure. Prefer to wire .env by hand? Copy .env.example to .env and fill in INKBOX_API_KEY, INKBOX_IDENTITY, INKBOX_SIGNING_KEY, and CLAUDE_PROJECT_DIR yourself.

On startup the bridge opens an Inkbox tunnel, wires mail/text/iMessage webhook subscriptions and the incoming-call channel to it, and routes everything into Claude Code sessions.

Running it

inkbox-claude run        # foreground (Ctrl+C to stop) — good for first runs and debugging

Or run it as a background daemon (PID + log under ~/.inkbox-claude/):

inkbox-claude start      # detach and run in the background
inkbox-claude status     # is it running? where are the logs?
inkbox-claude restart    # restart it
inkbox-claude stop       # graceful stop (SIGTERM, then SIGKILL after 5s)

tail -f ~/.inkbox-claude/gateway.log

start auto-loads .env from the current directory, so you don't have to source it first. run is the foreground version a service manager (systemd, Docker) should supervise; start/stop are the self-contained background option.

Start on boot

The setup wizard offers to keep the bridge running for you — either just in the background for this session, or as a service that starts on every boot. On Linux it installs a systemd user unit (~/.config/systemd/user/inkbox-claude.service) and enables it; on macOS it installs a launchd agent. To keep a Linux service alive while you're logged out, enable lingering once:

sudo loginctl enable-linger "$USER"
systemctl --user status inkbox-claude   # restart | stop | status

Uninstall

inkbox-claude uninstall           # stop it, remove the boot service + launcher; keep config
inkbox-claude uninstall --purge   # also delete ~/.inkbox-claude (config, logs, sessions)

This is local-only — webhook subscriptions on the Inkbox side are left as-is; remove them in the Inkbox Console if you want.

Then, from your phone:

  1. Text START to the agent's number (first time only, carrier opt-in).
  2. Text it something like "clean up the TODOs in the auth module".
  3. Approve the permission texts as they arrive. Get the result as a text.

How escalation works

Claude Code never silently runs anything destructive. The bridge passes a can_use_tool callback to the Claude Agent SDK:

  • Read-only tools (Read, Grep, Glob, WebFetch, …) and the Inkbox messaging tools run without asking. Override with INKBOX_AUTO_ALLOWED_TOOLS.
  • Everything else (Bash, Write, Edit, …) blocks the agent mid-turn while the bridge texts you a one-line plain-language summary of what Claude wants to do. Your next message answers the escalation instead of starting a new turn — reply 1/yes, 2/always (session-scoped grant), or 3/no.
  • AskUserQuestion polls are formatted as numbered options; reply with the number or free text.
  • No reply within INKBOX_PERMISSION_TIMEOUT_S (default 10 min) → the tool call is denied and Claude is told you didn't answer; it carries on as best it can.

Sessions

Direct sessions are keyed by Inkbox contact, so one person shares context across channels. SMS and iMessage groups instead use their conversation ID: all participants share one session, separate from direct messages and other groups. Reactions and delivery recovery stay in that group. Claude session IDs persist in ~/.inkbox-claude/sessions.json across restarts. Each turn keeps its original reply destination, including errors and permission prompts. Automatic email replies use reply-all on the stored inbound message, preserving its To/CC and thread; explicit send tools can still start a new email. If a voice call ends before Claude finishes a voice reply, that late voice reply is dropped instead of silently switching to SMS or email.

A2A worker progress. Inbound A2A tasks receive an immediate pickup acknowledgement, followed by short progress updates about every three minutes until the task settles. Set INKBOX_A2A_PROGRESS_INTERVAL_SECONDS to change the cadence or 0 to disable periodic updates.

Typing indicator. While Claude works on a turn, the bridge keeps a typing indicator alive on your iMessage thread (refreshed every few seconds, since it expires) so you can see it's busy. SMS, email, and voice have no typing indicator, so this is iMessage-only.

Delivery failures. An outbound message can die two ways, and the bridge feeds both into one delivery-failure loop. It can be rejected at send time — the server's content policy blocks it (markdown artifacts, emoji overload), the recipient has opted out, the address is bad, or the body is too long — which comes back as an error on the send call. Or it can be accepted and then fail downstream — a carrier filters the SMS, an iMessage is declined, an email bounces — which Inkbox reports asynchronously (text.delivery_failed/text.delivery_unconfirmed, imessage.delivery_failed, message.bounced/message.failed). Either way the bridge wakes the affected contact's session to tell Claude which message didn't land and why, so it can fix and resend or reach you another way using its Inkbox tools. The wake-up runs as a side-effect turn — Claude acts via tools rather than replying on the channel that just failed. Sends are hard-capped at three per logical reply with the budget shared across both surfaces (keyed by conversation/recipient): after that the thread goes quiet with a loud log line instead of looping. The budget resets on a fresh inbound, a delivered receipt, or a 30-minute TTL, and repeat webhooks for the same message are de-duplicated. Transient (5xx) send failures are excluded — a bare resend clears those.

Interrupt by texting again. Messaging the agent again while it's mid-turn works like pressing Esc in Claude Code and typing a new message: the running turn is interrupted, its partial answer is dropped, and Claude picks up your new message instead. (A reply while it's waiting on a permission/poll still answers that escalation — interrupting only applies while it's actively working.)

Control commands. A handful of slash-commands steer the conversation itself and are handled by the bridge instead of being sent to Claude (works on any channel):

  • /clear (or /new) — start a fresh conversation: forgets the resumed session, tears down the client, and clears session-scoped permission grants.
  • /stop (or /cancel) — interrupt the current turn and drop anything queued, keeping your conversation context intact.
  • /resume — texts you back a numbered list of recent conversations for the project (each with a short summary and timestamp); reply with a number to reopen that one. Like /resume in the Claude Code CLI.
  • /status — reports what the bridge is doing for you right now (working, waiting on a reply, or idle) and whether you're in a fresh or ongoing conversation. Read-only; doesn't disturb a running turn.
  • /usage — reports your Claude subscription usage, mirroring the Claude Code /usage command: the rolling 5-hour session window and the weekly windows, each with percent used and when it resets.
  • /health — reports bridge health: whether Inkbox is reachable (live identity check + which channels are live), the inbound tunnel is connected, and Claude is ready to run (SDK present, authenticated).

These match only when the whole message is exactly the command, so "please /clear the cache" is still a normal turn.

Errors. If a turn fails, you get a short plain-language heads-up ("I hit an error while working on that and had to stop") rather than silence.

Voice

The configured phone voice stack applies to inbound and outbound calls:

  • Inkbox Voice AI: Inkbox handles the conversation on the agent's behalf. When the call ends, a signed call.ended event wakes the same contact-keyed Claude Code session once to execute remaining commitments. Plain model text from that synthetic turn is suppressed; explicit tool side effects still run.

  • OpenAI Realtime (when configured): the bridge pre-opens an OpenAI Realtime session and accepts the call in raw-media mode, so a natural, low-latency voice handles the conversation. It runs the call itself and has these tools:

    • consult_agent — do real work now in the project; runs in the same contact-keyed session as your SMS/iMessage and its answer is spoken back.
    • register_post_call_action / edit_post_call_action / delete_post_call_action — queue, change, or cancel work to run after you hang up.
    • hang_up_call — two-step (say goodbye, then end the call).

    When the call ends, queued actions run in your session (and any plain "reflect on the call" follow-up if none were queued) — so "after we hang up, open a PR and text me" actually happens. Enable it in inkbox-claude setup (it validates your OpenAI key live) or via the INKBOX_REALTIME_* env vars below.

  • Inkbox STT/TTS (default / fallback): Inkbox auto-accepts the call and opens a WebSocket to the bridge; finalized transcripts become turns in your same session and Claude's replies are spoken back. The bridge falls back to this automatically if Realtime is off or OpenAI can't be reached (unless INKBOX_REALTIME_FALLBACK_TO_INKBOX_STT_TTS=false).

Two calling lines

Calls — inbound and outbound — can run over either of two lines, and the agent picks the one that matches the channel it's talking on:

  • The dedicated phone number. The agent's own number (the same line SMS uses). Outbound calls present this number; inbound calls to it ring the agent.
  • The shared Inkbox iMessage line. The agent can also place and receive voice calls with a person it's connected to over iMessage, over the same shared line that person already messages. The underlying number is never surfaced — Inkbox resolves it from the iMessage connection — and it only works for people already connected over iMessage (an unknown caller is rejected; an outbound call with no connection is refused).

Inbound answering is configured once per identity (hosted_agent for Inkbox Voice AI, otherwise auto_accept to the call bridge WebSocket), so a single setting governs both lines. Outbound, the agent sets origination on inkbox_place_call (dedicated_number / shared_imessage_number), or omits it — then it resolves to the only line available, or, when both are, to the line matching the current conversation's channel (an iMessage turn calls over the shared line; an SMS/phone turn over the dedicated number).

External webhooks

Beyond Inkbox's own events, the /webhook endpoint can wake the agent for events from other systems (e.g. a GitHub Actions failure). Every request is classified by its signature header first, then verified with that source's scheme — routing keys off who actually signed the request, never off the body's claimed event type, so a forged payload can't impersonate an Inkbox event:

  • Registered sources (GitHub via X-Hub-Signature-256 today; drop a new provider module into inkbox_claude/webhook_providers/ to add one) are verified with INKBOX_WEBHOOK_SECRET_<NAME> and always delivered — registering the provider + secret is the opt-in. The agent is told the event is verified and directed to act via its tools (its text reply on an external thread isn't delivered to anyone).
  • Unknown/unverified sources are dropped by default. Set INKBOX_EXTERNAL_EVENTS_ENABLED=true to pass them through anyway; the agent then gets a cautious directive forbidding irreversible action on the event's say-so alone.

Media

Inbound. When someone sends an MMS image, an iMessage attachment, or an email with files, the gateway downloads them to ~/.inkbox-claude/media/ (override with INKBOX_CLAUDE_MEDIA_DIR) and appends the local paths to the message, so Claude can open them with its Read tool — including viewing images. Media-only messages (no text) still wake the agent.

Outbound. Claude sends media with a single tool call per channel — it just passes local file paths, and the tool handles any upload-then-send round trip internally:

  • Email — inkbox_send_email(..., attachment_paths=[...]) (base64 inline, ~25 MB total).
  • iMessage — inkbox_send_imessage(..., media_path=...) (uploaded + sent, ≤10 MB).
  • SMS/MMS — inkbox_send_sms(..., media_paths=[...]) (uploaded + sent; media_urls also accepts already-hosted URLs).

Companion mode

Version 0.2.13 supports Companion mode with Inkbox SDK 0.7.6 or newer. It stays off until an administrator enables it for the identity and selects a sponsor. Use an agent-scoped API key. The bridge loads every initialization-history page, including attachment references and notices, before processing the current receipt. Historical sponsor messages do not trigger separate replies. Sessions are isolated by environment, identity, channel, conversation scope, and activation; a new activation starts separate context. Companion history is excluded from contact-memory injection.

Two independent settings control when a model turn starts:

  • INKBOX_GROUP_REPLY_MODE=auto|mention (default auto). In mention mode, the current message must contain a whole @agent or @your-handle mention, case-insensitively. Links, email addresses, and previous messages do not count. For Companion email, putting the agent's mailbox in the current To list also counts; Cc and Bcc alone do not.
  • INKBOX_COMPANION_RESPONSE_MODE=safe|relaxed (default safe). Safe mode wakes only for a current sender_access="direct" message. Sponsored messages and messages without recognized access metadata remain context-only. Relaxed mode permits any delivered sender to wake the agent, subject to mention mode. direct means the message passed contact rules without sponsorship, including allowed-by-default senders—not permanent trust or permission to execute tools.

Quiet messages are durably buffered without starting the model, using tools, typing, interrupting a running turn, or changing its reply destination. They are included as context in the next eligible turn. The setup wizard offers both settings for new and reconfigured identities and preserves existing choices. Mention mode also applies to ordinary SMS/iMessage groups. Ordinary slash controls and valid approval answers from the asked sender remain mention-exempt.

Automatic replies and inkbox_reply_companion use the saved sponsor email reply-all anchor or signed MMS/iMessage conversation. The bridge reuses that route and its startup identity without loading activation history again for each turn or reply. Optional local sender rules still apply without a network lookup, as do existing send requirements. Group iMessage requires a supported dedicated line. Companion mode does not grant permission to send a separate direct message or place a call.

INKBOX_COMPANION_MAX_BYTES defaults to 200000. It bounds snapshot loading and the assembled UTF-8 input. If history exceeds the limit, initialization fails explicitly before a Claude query. Nothing is truncated or split into extra turns. Choose a limit that fits your model's context, including its system prompt and tools; bytes are not a token estimate. Attachment references and history-gap notices remain in the input.

Companion state is stored under ~/.inkbox-claude/companion/, or the corresponding directory under INKBOX_CLAUDE_HOME. Keep this state when upgrading. Pending hydration and queued messages recover on restart. /health exposes counts by Companion state; each checkpoint records a logical submission ID and any error. Only one bridge process can own an identity's Companion state in that directory. Invalid checkpoints are preserved with an .invalid suffix and only that scope is paused; unrelated conversations and channels can still start. Keep quarantined files until an operator reconciles the uncertain state, rather than deleting them to admit duplicate work. Delivery-failure callbacks for quarantined conversations are held instead of starting private recovery. If corruption also destroys the routing information, all delivery-failure callbacks are held until it is repaired; ordinary inbound messages and healthy Companion scopes still work. Temporary startup, hydration, and pre-submission failures retry automatically with delays capped at 30 seconds. Network failures and HTTP 429/5xx remain retryable; other unexpected startup or hydration failures pause after five retries for operator review. Nonretryable preparation failures for an already completed answer pause immediately, retaining the answer. Completed model results are checkpointed before sending, so a known pre-send failure retries delivery without rerunning the model. If activation loading reports unavailable access, captured message content is discarded while deduplication records remain. Stop the bridge before moving its state directory. Delivery failures for tracked conversations are logged and saved as last_delivery_failure in their checkpoints for operator review. They do not start an automatic retry turn or a private contact conversation.

If a query's acceptance, completion, or send outcome is uncertain, the request is retained without replay. The conversation stays paused while the old native owner cannot be fenced; once fencing is proved, the uncertain request becomes unconfirmed and later fresh work can proceed. Stop the bridge and inspect the Claude transcript using the checkpoint's logical input ID before manually reconciling an unresolved outcome. Deleting checkpoints or resetting uncertain requests to pending can repeat actions. An oversized or unavailable activation is marked failed; after correcting a pre-submission failure, an operator may reset its state to pending while the bridge is stopped, then restart to revalidate it.

Historical commands and approval-like text remain conversation data. Only a new live message from the sender who was asked can answer a Companion permission request or question; it must pass both current sender-access and addressing policies. The prompt explains the mention requirement. Sponsor slash controls use the same gates. Email author comparisons are case-insensitive; phone authors match exactly. Tracked ordinary messages use their own group conversation, separate from active Companion context.

Config reference

Env var Required Default Description
INKBOX_API_KEY yes - Agent-scoped Inkbox API key.
INKBOX_IDENTITY yes - Inkbox agent identity handle.
INKBOX_SIGNING_KEY inbound - Webhook HMAC secret for signed inbound events.
CLAUDE_PROJECT_DIR yes cwd Directory Claude Code works in.
CLAUDE_MODEL no CLI default Model override for bridged sessions.
INKBOX_REQUIRE_SIGNATURE no true Refuse unsigned inbound webhooks unless false.
INKBOX_SKIP_WEBHOOK_RECONCILE no false Leave webhook subscriptions untouched on start. For deployments that provision them ahead of time, where the destination is fixed or this API key may not change it. They must already point at this bridge's webhook URL, or nothing arrives.
INKBOX_EXTERNAL_EVENTS_ENABLED no false Wake the agent on unrecognised/unverified external webhooks (see External webhooks).
INKBOX_CONTACT_MEMORIES_ENABLED no true Include matched-contact memories as background context for human conversations and calls.
INKBOX_GROUP_REPLY_MODE no auto auto or mention; only current explicit mentions wake group turns.
INKBOX_COMPANION_RESPONSE_MODE no safe safe requires current direct admission; relaxed permits any delivered sender.
INKBOX_COMPANION_MAX_BYTES no 200000 Maximum bytes for Companion snapshot loading and one assembled input.
INKBOX_A2A_PROGRESS_INTERVAL_SECONDS no 180 Seconds between short progress updates for active inbound A2A tasks; 0 disables periodic updates.
INKBOX_WEBHOOK_SECRET_<NAME> per source - Verification secret for a registered third-party webhook source (e.g. INKBOX_WEBHOOK_SECRET_GITHUB).
INKBOX_BASE_URL no SDK default Override the Inkbox API base URL.
INKBOX_PUBLIC_URL no - Public bridge URL. Omit to use an Inkbox tunnel.
INKBOX_TUNNEL_NAME no identity handle Tunnel name override.
INKBOX_ALLOWED_USERS no - Local allowlist (emails / E.164 numbers). Usually leave empty and use Inkbox contact rules.
INKBOX_ALLOW_ALL_USERS no false Allow all senders admitted by Inkbox contact rules.
INKBOX_BRIDGE_PORT no 8767 Local webhook server port.
INKBOX_PERMISSION_TIMEOUT_S no 600 Seconds to wait for a permission/poll reply.
INKBOX_AUTO_ALLOWED_TOOLS no read-only set Tools that never need a permission text.
INKBOX_VOICE_STACK no inkbox_tts_stt inkbox_voice_ai, openai_realtime, or inkbox_tts_stt.
INKBOX_VOICE_AI_AUTHORITY_MODE no contact_scoped Local mirror used by doctor for the saved Voice AI authority.
INKBOX_VOICEMAIL_DETECTION no enabled Default outbound policy: enabled or disabled.
INKBOX_REALTIME_ENABLED no false Use OpenAI Realtime for calls. Needs a key; off → Inkbox STT/TTS.
INKBOX_REALTIME_API_KEY realtime OPENAI_API_KEY OpenAI key with /v1/realtime access.
INKBOX_REALTIME_MODEL no gpt-realtime-2 Realtime model id.
INKBOX_REALTIME_VOICE no cedar Realtime voice name.
INKBOX_REALTIME_FALLBACK_TO_INKBOX_STT_TTS no true Fall back to Inkbox STT/TTS if OpenAI connect fails.

Tools exposed to Claude

The agent reaches you (or third parties) through an in-process MCP server:

  • inkbox_whoami — its own identity: handle, mailbox, and its two calling lines (dedicated phone number + shared iMessage line status).
  • inkbox_send_email — send email; attach local files with attachment_paths.
  • inkbox_reply_companion replies to the current Companion group with its fixed reply target.
  • inkbox_send_sms — send SMS/MMS; attach local files with media_paths (or hosted media_urls).
  • inkbox_send_imessage — send into an iMessage conversation; attach a local file with media_path.
  • inkbox_place_call — place an outbound voice call through the running gateway with purpose/opening/context, over either line via origination (see Two calling lines).
  • inkbox_list_calls · inkbox_get_call_transcript — browse recent calls and fetch transcript segments.
  • inkbox_list_text_conversations · inkbox_get_text_conversation — browse SMS threads and history.
  • inkbox_list_imessage_conversations · inkbox_get_imessage_conversation — browse iMessage threads and history (find the conversation_id to send into).
  • inkbox_lookup_contact · inkbox_list_contacts · inkbox_get_contact — resolve and read address-book contacts (reverse-lookup by email/phone, free-text search, or full record by id).
  • inkbox_create_contact · inkbox_update_contact · inkbox_delete_contact — save, edit, and remove organization-wide contacts. Changes affect the shared address book. vCard export/import is not exposed.
  • inkbox_a2a_call · inkbox_a2a_check · inkbox_a2a_reply — delegate work to another agent and follow its task.
  • inkbox_list_a2a_tasks · inkbox_list_a2a_messages — page and search this identity's inbound and outbound A2A history, with participant, task, context, role, state, and timestamp filters.
  • inkbox_a2a_complete · inkbox_a2a_ask_caller · inkbox_a2a_fail — commit the outcome of a verified inbound A2A task. These tools are rejected outside that task's isolated session.

The bridge requires Inkbox SDK 0.7.6 or newer, below 1.0.0.

Phone call voice stack

inkbox-claude setup presents a Phone call voice stack section with three choices:

  1. Inkbox Voice AI handles the call and wakes Claude Code after it ends. Choose contact-scoped or YOLO authority during setup. Authority changes require an admin API key, but that credential is used only in memory and is never written to the plugin .env.
  2. OpenAI Realtime API uses a separately validated Realtime API key for low-latency call audio while Claude Code handles complex work.
  3. Inkbox TTS/STT keeps the full agent turn in Claude Code and uses Inkbox speech services, with higher latency.

The canonical selection is INKBOX_VOICE_STACK (inkbox_voice_ai, openai_realtime, or inkbox_tts_stt). INKBOX_VOICEMAIL_DETECTION accepts enabled or disabled and applies to outbound calls unless the tool call explicitly overrides it. Voice AI outbound calls send a reason and use the saved server-side authority default; the plugin does not elevate authority per call.

Local/manual Docker test environment

This image is for interactive local testing. It installs Claude Code, this plugin, and the pinned Inkbox SDK, but contains no credentials. It uses your native Claude login; an Anthropic or OpenAI API key is not embedded in the image. An OpenAI key is only needed if you select OpenAI Realtime for calls.

docker build -f Dockerfile.manual-test -t inkbox-claude-manual .
docker run -d --name inkbox-claude-manual \
  -v "$HOME/.claude:/home/node/.claude" \
  -v inkbox-claude-state:/home/node/.inkbox-claude \
  -v "$PWD:/workspace" \
  inkbox-claude-manual
docker exec -it inkbox-claude-manual bash
claude --version
inkbox-claude setup
inkbox-claude doctor
inkbox-claude run

On a live call, the OpenAI Realtime voice agent additionally gets consult_agent, register_post_call_action / edit_post_call_action / delete_post_call_action, and hang_up_call — see Voice.

Smoke test

  1. inkbox-claude doctor — everything green.
  2. Text START, then text the agent; verify it replies in the same thread.
  3. Ask it to do something requiring a command (e.g. "run the tests") and verify you get a permission text; reply 1 and verify the result comes back.
  4. Ask it something open-ended enough to trigger a poll; reply with a number.
  5. Email the agent; verify the reply lands as an email on the same thread.
  6. Call the number, ask what it's working on, hang up mid-answer, and verify the late voice tail is not silently sent as SMS or email.

Development

Development installs and CI use the published Inkbox SDK (>=0.7.6,<1.0.0), including typed sender_access on initialization-history entries. Contract tests also exercise the latest Claude Agent SDK and Claude Code CLI.

python -m pytest

Architecture notes

  • Tunnel-first inbound: with a signing key, the gateway opens an Inkbox tunnel, reconciles mail/text/iMessage webhook subscriptions, and sets the identity's incoming-call action (auto_accept + call WebSocket) covering both calling lines.
  • Contact-keyed sessions: webhook payloads carry resolved contacts; a single resolved contact id becomes the session key, otherwise the raw address/number does. One human, one session, every channel.
  • Escalation over the active channel: a pending permission/poll captures the contact's next inbound message as its answer, on whichever text channel they're using.
  • Claude Agent SDK: each session is one ClaudeSDKClient (its own Claude Code subprocess) with the claude_code system-prompt preset plus a messaging channel prompt appended, can_use_tool for escalation, and an in-process MCP server for the Inkbox tools.

HD realtime voice

Realtime calls request 16 kHz mono PCM16 call audio. The bridge continuously resamples to and from the realtime session's 24 kHz PCM format, preserving audio across WebSocket frame boundaries. Older call streams that advertise 8 kHz μ-law (or omit their audio descriptor) remain supported. Call audio quality also depends on the remote connection. Hosted voice and managed speech modes are unchanged.

Slack, native iMessage, and Vault

The bridge requires Inkbox SDK 0.7.15+. Native iMessage and Slack Companion also require matching server capabilities. Native Slack task streams are optional and require an SDK exposing the complete task-stream interface (0.7.16+).

Slack

For a requested image or file, the agent can use inkbox_slack_upload_file with a local file of 1 byte–10 MiB. A Markdown image or local path alone does not upload a file. Uploads preserve the supplied conversation and thread; uncertain results must be inspected with inkbox_slack_get_operation, not resent.

Slack replies use channel-specific mrkdwn guidance. While tools run, concise progress appears as a native task card when supported, or as one ordinary message edited in place. Inline DMs remain inline. Progress never includes tool arguments, local paths, or worker identifiers, and stops updating when the original turn ends. An unconfirmed write never causes a second progress message or blind replay.

Run inkbox-claude setup to opt in, select or configure a saved workspace, prepare its agent app, open the installation link, and wait for a confirmed connection. Setup preserves an existing connection and never silently switches credentials. Alternatively set INKBOX_SLACK_ENABLED=true for an already configured identity. Slack events always require a valid Inkbox webhook signature, even if general webhook signature enforcement is disabled.

Ordinary channel mentions reply in a native thread; ordinary DMs reply inline. Companion keeps one channel-wide conversation across native threads, but replies, approval answers, and Stop stay attached to the current message's exact route. Native Stop receipts persist their exact actor/thread-owned targets before interruption; a redelivered Stop cannot cancel a later request. Current connection ownership, workspace and connected status are checked before admission, replies and indicator cleanup. A top-level Companion reply stays top-level. INKBOX_GROUP_REPLY_MODE=mention requires a native agent mention in a group; INKBOX_COMPANION_RESPONSE_MODE=safe requires current direct admission. These independent settings compose. Quiet messages remain context without starting tools, generation, or a work indicator.

Inline replies use 👀 while working and ❌ on failure. Native subthreads use Slack's working/awaiting-input status, without reaction fallback. Tool progress is shown separately as task cards or one edited message. The connection must support the relevant Slack API capability. API success alone cannot verify the indicator's appearance in a particular Slack client.

The Slack tools support connections, conversations, bounded message history, retained-text search, explicit sends, file uploads, and action/operation lookup. Attachment references are metadata, not downloaded files. Explicit sends require a stable idempotency key; inspect an uncertain action instead of sending it again. Disabling Slack blocks its tools, new input, recovered work, and automatic delivery.

Native iMessage replies

Opt in by setting INKBOX_IMESSAGE_THREADED_REPLIES=true in the bridge environment and restarting. This is deliberately not an additional setup wizard prompt. Every message-triggered reply targets its admitted source. Short compatible bursts collect after 750 ms of quiet, capped at two seconds, and target their first source. Further messages queue behind active work instead of interrupting it. Media, reactions, different senders, and different native reply contexts do not combine. Bursts are limited to eight sources and 4000 combined text characters; overflow stays queued with its own source anchor. Source-bound send/read tools cannot change the native reply target, upload before route validation, or expand Companion history. Stop affects owned iMessage work, not an unrelated voice consult for the contact.

Native IDs remain opaque and nullable. The bridge performs identity-scoped source and thread preflight reads before targeted sending. Only the API may fall back to a plain same-conversation message. A failed targeted send never causes a second, unthreaded send. A deliberately requested send to a different conversation or to uses ordinary SDK routing, without inheriting the inbound native source or suppressing its answer. It still requires the originating turn to remain active. Triggerless proactive sends retain ordinary SDK behavior. The model cannot override the bridge's target/fallback policy. Companion uses its existing ordered durable receipt owner rather than ordinary burst collection. With native replies enabled, asynchronous delivery failures are retained as bounded, quiet context, including callbacks that precede the accepted-send response. They do not start model work or authorize a resend. Accepted sends remain accepted if local delivery tracking is temporarily unavailable.

Vault and 2FA

The agent can list identity-accessible secret metadata, retrieve one selected credential, or obtain a current login 2FA code. Set INKBOX_CLAUDE_VAULT_KEY locally to enable lazy unlocking. Never put it in a conversation or tool argument. Metadata and unrelated tools remain available with a missing or wrong bridge key. Every credential/code invocation validates the current bridge-local key and freshly checks the selected secret's identity access before and after decryption. Cached or SDK-global unlocked state cannot bypass a missing/changed key or revoked access. Credential output omits TOTP seeds; code output contains only the current code and its validity timing.

Migrate any SDK-global INKBOX_VAULT_KEY or vault_key in ~/.inkbox/config to the bridge-specific environment setting. The SDK eagerly unlocks global keys at client construction; it has no public opt-out in the supported release. The bridge does not rewrite global SDK settings or monkeypatch SDK internals.

Readiness and retained work

/health remains a listener/liveness endpoint. /ready additionally checks native Claude CLI authentication, initialized session storage, and blocked conversation owners. inkbox-claude doctor uses the same bounded native auth check. Neither endpoint claims a model task or external reply was completed; use a live acceptance check for that proof. If Claude reports a stale login before any execution, the bridge reconnects once without discarding the selected session and then gives local login guidance. An auth failure after assistant/tool progress or a nonzero host-turn count is uncertain and is never retried as a login refresh.

Ordinary Slack/native-iMessage receipts and completed answers are persisted before acknowledgement or send. Companion journals, quiet context, Claude session IDs, and hosted-SMS reservations remain intact across upgrade. Temporary transport failures positively identified before host submission or during read-only reply preflight retry automatically with capped backoff, keeping the original route and working indication. A saved answer retries delivery preparation only, not the model task; Stop cancels its owned retry. A nonretryable check before the send checkpoint retains the answer for explicit recovery without sending or retrying it. Only positively matched terminal saved answers may be reused after a host interruption. Otherwise an uncertain request is retained without automatic re-execution; later fresh work can proceed only after the old native host and side effects are fenced. An uncertain send is never automatically replayed. If ownership cannot be proved, readiness reports a blocked scope for operator inspection. That block survives repeated restarts until ownership fencing is positively confirmed. A missing or reused native parent PID is not proof of cleanup: its older child snapshot may omit later orphaned tools, so that scope stays quarantined. A live owned process is fenced before SDK disconnect removes its observable identity. Successful shutdown fences are persisted for the exact stopped owner, allowing fresh work after restart without replaying its interrupted request. A failed proof write retains the exact fence for a later persistence retry without releasing unrelated unconfirmed owners. Failed reset or resume controls leave the conversation paused and preserve the existing session and context. Revoked or terminally failed Companion records remain deduplication tombstones and do not by themselves make the bridge unready.

State lives under INKBOX_CLAUDE_HOME (default ~/.inkbox-claude). Stop the bridge before backing up or moving it. Do not delete journals or session IDs to recover a paused scope. Older releases do not understand the new unconfirmed scheduling state; preserve the state directory and use this release for recovery rather than assuming a downgrade can safely replay it.

About

Give your Claude Code agent its own Inkbox identity: a mailbox, iMessage, a phone number for calls and SMS, and an internet address. Talk to it from anywhere.

Topics

Resources

Stars

19 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages