Beta: Plugins use Delta CLI, which is in beta, so the commands and output your plugin relies on can change. The CLI reports which versions of that output it supports, so your plugin can stop and ask for an update instead of breaking. See Connect a session.
Build a plugin that connects a coding agent to Delta, so its users can share sessions and join teammates’ threads the way the Claude Code Plugin does. The plugin records the agent’s conversation and files in a Delta thread through the CLI. The open-source Claude Code plugin is a complete example.
Delta handles authentication, storage, synchronization, and checkouts. Your plugin is responsible for:
- Translating your agent’s activity into records and sending them to Delta.
- Saving records until Delta confirms them.
- Giving your agent the activity it receives from teammates.
- Stopping your agent’s turn when someone else takes over the thread.
Use the CLI for everything. Don’t read or write Delta’s database directly.
Connect a session
Your plugin needs the Delta CLI and a signed-in account. First, check that your plugin and the installed CLI are compatible:
delta --json thread contract
The response reports the contract versions the CLI supports, such as
{"operation":"contract","min":8,"max":8}. This page describes contract
version 8, which Delta 0.22.0 introduced. If max is below 8, ask the user to
update Delta. If min is above the version your plugin was built for, ask them
to update your plugin. This version is separate from the schemaVersion in
each record.
Each session needs two IDs: --source, a fixed name for your plugin, and
--external-id, your agent’s ID for the session. Delta uses the pair to find
the session’s thread, so save both with the thread ID.
Run this example from the root of a Git checkout. It requires jq and a
selected organization:
set -eu
SOURCE=example-agent
# Use your agent's own session ID, and reuse it when you retry.
SESSION_ID=example-session-1
THREAD_ID="$(delta --json thread create \
--source "$SOURCE" --external-id "$SESSION_ID" | jq -er '.thread_id')"
printf 'Save this thread ID with your session: %s\n' "$THREAD_ID"
REFERENCE="$(delta --json worktree create "$PWD" | jq -er '.reference')"
delta --json worktree attach "$REFERENCE" --thread "$THREAD_ID"
delta --json worktree mount "$REFERENCE" "$PWD" --existing-checkout > mount.jsonl &
MOUNT_PID=$!
delta --json thread share "$THREAD_ID" --visibility organization
Ask before sharing, and tell the user which conversation content and files
their organization will see. Save the thread ID and the worktree reference
before you attach or share. Running thread create again with the same two IDs
returns the same thread, so a retry is safe. To reuse a checkout’s worktree on
a retry, find its reference in delta --json worktree list instead of
creating another.
worktree attachonly adds the worktree to the thread. It doesn’t upload or sync files.worktree mount --existing-checkoutuploads the checkout and keeps syncing it for as long as the command runs. It prints one JSON line when it’s ready. Keep it running for the whole session.- When the session ends, stop the mount with
kill -TERM "$MOUNT_PID". Delta spends a short time uploading changes it hasn’t sent yet, then stops syncing. A mount whose process is gone doesn’t keep Delta running.
To share with specific people instead of the whole organization, invite them
in place of thread share:
delta --json thread invite "$THREAD_ID" ada@example.com
If the thread isn’t uploaded yet, thread invite uploads it privately, so
only the user and the invited people can open it. The response has a result
for each address, and one failed address doesn’t fail the command.
To check access, attached worktrees, and the next sequence number to send:
delta --json thread status "$THREAD_ID" \
--source "$SOURCE" --external-id "$SESSION_ID"
Use this to check access rather than assuming a link grants it.
Reference: sharing, CLI compatibility, and output parsing.
Send conversation updates
thread append reads records as UTF-8 JSON Lines on stdin. Each record has
schemaVersion, sequence, an optional turnSequence, and an ACP v2
update. Appending records activity. It doesn’t run messages or tool calls.
Share the thread before you append to it, or create it with an upload
organization as described in Record while offline.
To record the first message in the thread created above:
printf '%s\n' \
'{"schemaVersion":1,"sequence":1,"update":{"sessionUpdate":"user_message","messageId":"message-1","content":[{"type":"text","text":"Please inspect this change."}]}}' \
| delta --json thread append "$THREAD_ID" \
--source "$SOURCE" --external-id "$SESSION_ID" \
--producer-name "Example Agent"
--producer-name is the name Delta shows for your agent’s output. Without it,
Delta shows --source.
Supported updates are user_message, agent_message, agent_thought,
tool_call_update, state_update, and session_info_update, which sets the
thread’s title.
Streaming chunk updates are rejected. To stream a message, send it again with
the same messageId and its full content so far. Recording a tool call’s diff
doesn’t change files in the checkout.
To record a live turn, start it with a running state update whose
turnSequence equals its own sequence. Give the turn’s content and its
final idle update the same turnSequence. Continuing after the record above:
{"schemaVersion":1,"sequence":2,"turnSequence":2,"update":{"sessionUpdate":"state_update","state":"running"}}
{"schemaVersion":1,"sequence":3,"turnSequence":2,"update":{"sessionUpdate":"agent_message","messageId":"answer-1","content":[{"type":"text","text":"The change adds validation."}]}}
{"schemaVersion":1,"sequence":4,"turnSequence":2,"update":{"sessionUpdate":"state_update","state":"idle"}}
A state_update without turnSequence is recorded as history. It doesn’t
start or finish a turn.
To end the thread’s current turn without starting another, as Delta’s Stop
button does, run delta --json thread stop "$THREAD_ID".
Reference: conversation translation and initial export.
Receive shared activity
delta --json thread watch "$THREAD_ID" \
--source "$SOURCE" --external-id "$SESSION_ID" \
--participate "Example Agent"
Passing --source and --external-id hides the items your plugin recorded.
Omit both to see everything. Watching doesn’t create, share, mount, or run
anything.
--participate shows the user to collaborators as taking part through your
agent, under the name you pass, for as long as the watch runs. Without it, the
watch is invisible to collaborators.
Keep this watch open while your agent’s turn runs. If your agent’s process
exits while its turn is still running, Delta ends the turn when the watch
ends, so a crash doesn’t leave the thread busy for everyone. The watch treats
its parent process as your agent; pass --owner-pid to name another one.
The stream starts with ready, then the thread’s current items, then live,
then changes as they happen. Keep your own copy of the items, keyed by ID:
itemreplaces an item;item.appendextends its text;removedeletes it.- Order items by
after_id, retaining unresolved predecessors until they arrive. turn,presence, andworktreesreplace their latest state.producer.turnreports your turn’s state, including supersession.commentandcomment.removeupdate delivered comments.childrenreplaces the list of subagents and subthreads. It arrives once, afterlive, so don’t wait for it before treating the thread as ready.childadds or replaces one entry bythread_id, andchild.removedeletes one.
To follow a child, pass its reference unchanged as the thread argument to
watch, status, append, or mount. Don’t build a reference
from a thread_id.
Allow new fields and skip unknown event types. Add remote activity to your agent’s context without automatically starting a turn.
Reference: remote context and display.
Prepare files for joining
When a user joins an existing thread, check their access, mount the thread’s
files, and record only new activity from then on. Don’t export their previous
session, attach its checkout, or share the thread again. Keep the share link
for reconnecting, but use the thread_id that commands return as the thread’s
ID.
Mount all of the thread’s worktrees for your agent session. THREAD accepts a
share link, a share ID, or a thread ID:
THREAD="https://delta.dev/join/thread_..."
# A stable ID for this session's mounts. Reuse it to recover them later.
OWNER="$SESSION_ID"
delta --json thread mount "$THREAD" --owner "$OWNER" > thread-mount.jsonl &
MOUNT_PID=$!
thread mount keeps running for as long as your session needs the files.
When the checkouts are ready, it prints one JSON line with the thread’s
directory, a folder containing a link to each worktree’s checkout, and the
checkouts themselves. Move your agent into that directory, and keep the
command running until the session ends. When worktrees are added to or
removed from the thread, the links in that folder change to match.
A worktrees watch event means the thread’s attachments changed, not that
new checkouts are ready. Start thread mount again with the same owner, wait
for its ready line, then stop the previous mount process. A live frame or an
idle turn doesn’t guarantee that a teammate’s latest file changes have
reached your checkout.
To release the mounts, stop the command:
kill -TERM "$MOUNT_PID"
Delta spends a short time uploading changes it hasn’t sent yet, then deletes
the checkouts it created. The same happens if your agent’s process exits
without stopping the mount. If the upload doesn’t finish, for example because
the computer is offline, Delta keeps the checkouts. Run thread mount again
with the same owner to recover them. Ignored files and local Git state, such
as branches, aren’t shared or kept.
Reference: joining and lifecycle rules.
Record subagents
If your agent starts subagents, record each one as a child thread under the
session’s thread, with the subagent’s own ID as --external-id:
CHILD_REFERENCE="$(delta --json thread create \
--source "$SOURCE" --external-id "$SUBAGENT_ID" \
--parent "$THREAD_ID" --name "Inspect the parser" \
--shared-worktrees | jq -er '.reference')"
Use the returned reference as the thread argument for the child’s append,
watch, status, and mount commands. Running the command again
with the same IDs returns the same child.
--shared-worktreesgives the child the parent’s checkouts, for a subagent that works in the same folder.--isolated-worktreesgives the child private copies of the parent’s worktrees, including uncommitted changes. Mount them withthread mount, then merge the child’s changes back withdelta thread merge "$CHILD_REFERENCE" --into "$THREAD_ID".- With neither option, the child has no checkouts.
A subagent’s prompt usually comes from its parent agent. To show it as sent by
the parent rather than the user, add the parent’s external ID to the
user_message record’s _meta:
printf '%s\n' \
'{"schemaVersion":1,"sequence":1,"update":{"sessionUpdate":"user_message","messageId":"prompt-1","content":[{"type":"text","text":"Inspect the parser"}],"_meta":{"delta":{"sender":{"external_id":"example-session-1"}}}}}' \
| delta --json thread append "$CHILD_REFERENCE" \
--source "$SOURCE" --external-id "$SUBAGENT_ID" \
--producer-name "Example Agent"
Recording a child doesn’t start a Delta turn. Your agent still runs the subagent.
Reference: subagent recording.
Recover safely
- Save records before sending them. Number them consecutively from 1, and never reuse a sequence number for different content.
- Retry with the same records. A command can fail after Delta recorded
its records. Resending them unchanged is safe and returns
duplicate. Delete a saved record only after Delta confirms it. - Expect partial batches. If a batch fails partway, the records before the
failure are still recorded. Check the receipt for each record:
recorded,duplicate, orfenced.fencedmeans the record belongs to a turn that someone else took over, and nothing was written. - Read errors from the JSON output. With
--json, retry only whenerror.retryableistrue, waitingretry_after_mswhen it’s present. Otherwise, pause and show the error. Logreason, but don’t make decisions based on it, because its values can change. - Stop your agent when its turn is taken over. Stop the local turn when a
producer.turnevent reportssupersededor a current-turn record comes backfenced. Keep late output’s originalturnSequence. Stopping can’t undo what tools already did. - Rebuild your view on reconnect. Each
thread watchstarts with a fresh snapshot, and there is no way to resume a previous stream. Track what you already gave your agent so a reconnect doesn’t repeat it. - Keep your saved state. If you lose your saved records or sequence numbers, you can’t restart the same session from sequence 1. Start a new session ID instead, which creates a new thread.
Reference: outbox storage, retry policy, and CLI fixtures.
Record while offline
Your plugin can keep recording without a connection to the server:
THREAD_ID="$(delta --json thread create \
--source "$SOURCE" --external-id "$SESSION_ID" \
--organization ORGANIZATION_ID --visibility private | jq -er '.thread_id')"
delta --json thread append "$THREAD_ID" \
--source "$SOURCE" --external-id "$SESSION_ID" --acknowledge local
--organizationand--visibilityonthread createrecord where the new thread should upload, using an organization ID fromdelta org list. The command returns without waiting for the network, Delta uploads the thread in the background, and you can append to it right away.--visibilityisprivateororganizationand applies only to the first upload. Usethread shareto share a private thread later.--acknowledge localreturns each receipt once the record is saved on this computer, instead of waiting for the server.
thread status also reports visibility and an upload object.
upload.state is queued, uploading, uploaded, or failed, and
upload.error explains the last failure.
If the server rejects changes recorded while offline, because someone else
changed the thread, appends fail with reload_required until the user
resolves it. Pause recording and tell them to open the thread in the Delta app
and choose Fork & Restore or Discard Changes. From the CLI, they can
run delta thread fork and then delta thread discard --force.
Reference: the Claude Code plugin’s automatic uploads, in
activation.ts
and project-policy.ts.
Adapt the reference to your host
Claude’s function hooks, /clear, /cd, trust prompt, and transcript APIs
are host-specific. Replace them with your tool’s session, cancellation,
folder-selection, context, and UI APIs.
Test interrupted sharing, lost receipts, reconnects, late superseded output, and resumed sessions. Test a handoff between two computers: one edits files and finishes a turn, then the other reads those files.
Next steps
- Claude Code Plugin: see the reference workflow from a user’s point of view.
- Get Started with Delta CLI: the commands your plugin builds on.