API reference
The contract for any Dejima client — Scusi, mobile/PWA apps, chat bridges, your own tools. The CLI targets this same API; nothing it does is off-limits to third-party apps.
- Connecting
- SDKs (Python & TypeScript)
- Authentication
- Errors
- Island endpoints
- Lifecycle endpoints
- Sessions (websocket)
- Agents
- Resources & rename
- Lightweight access (exec / cp / logs)
- Remote dev — SSH façade & editors
- Credentials & GitHub identities
- Brokered host files (Port)
- Capability brokering
- MCP brokering
- Host terminals
- Safety switch (panic)
- Operator: self-update
- Events & webhooks
- Server overview
- Agent-event endpoint (shim-only)
- Data types
- Event types
- End-to-end example
Connecting
The Dejima daemon (dejimad) speaks JSON over HTTP/1.1 and websockets. Three reachable surfaces, all serving the same API:
| Scenario | Endpoint | Notes |
|---|---|---|
| Same machine as the daemon | ~/.dejima/dejimad.sock (Unix socket) |
Always on. Auth = filesystem permissions (0600, only the user can connect). Pass to curl with --unix-socket. |
| Remote machine on the same tailnet | http://<host>:7273 |
Daemon must be started with --tcp :7273 (or via dejima service install). Only accepts connections from Tailscale-assigned IPs. Off-tailnet connections are refused at the listener. |
| Remote without Tailscale | SSH-tunnel the socket | ssh -L /tmp/dejimad.sock:/home/you/.dejima/dejimad.sock host, then point your client at the local tunnel. |
| Into an island (shell / editor) | SSH façade — dejimad --ssh <addr> |
Not the JSON API: the daemon is a single SSH front door (username = island name, per-island public-key auth) that bridges into the container. Powers shell, sftp, and VS Code / Cursor Remote-SSH straight into /workspace. See Remote dev. |
The CLI uses DEJIMA_HOST: unset → Unix socket; set to host:port → remote TCP. Apps follow the same convention or pick their own.
curl examples
# Local
curl --unix-socket ~/.dejima/dejimad.sock http://dejimad/v1/healthz
# Remote (on tailnet)
curl http://mac-mini.tailnet.ts.net:7273/v1/healthz
SDKs (Python & TypeScript)
Official thin clients wrap every endpoint below plus the websocket PTY stream — you get the REST surface and attach() without hand-rolling the envelope framing. Both read DEJIMA_HOST / DEJIMA_TOKEN from the environment, raise a typed error on non-2xx, and are generated against openapi.yaml. alpha — 0.x, fields may change until 1.0.
Python — pip install dejima-sdk
pip install dejima-sdk # REST client
pip install 'dejima[ws]' # + attach() PTY streams
from dejima import Client
dj = Client() # or Client(host="100.84.12.7:7273", token="…")
isl = dj.create_island(repo="git@github.com:you/foo.git", agent="claude-code")
dj.add_agent(isl["name"], type="codex")
print(dj.exec(isl["name"], ["git", "status", "--short"]))
with dj.attach(isl["name"]) as s: # websocket PTY, multi-attach
s.send(b"ls -la\n")
print(s.recv()) # bytes, or None when closed
TypeScript / JavaScript — npm install dejima
npm install dejima # Node 18+ (ESM)
import { Client } from "dejima";
const dj = new Client(); // or new Client({ host: "…", token: "…" })
const isl = await dj.createIsland("git@github.com:you/foo.git", { agent: "claude-code" });
await dj.addAgent(isl.name, { type: "codex" });
console.log(await dj.exec(isl.name, ["git", "status", "--short"]));
const s = await dj.attach(isl.name); // websocket PTY, multi-attach
s.sendText("ls -la\n");
s.onData(bytes => process.stdout.write(bytes));
Source & full coverage list: sdk/python · sdk/ts. Another language? Everything below is one HTTP/WebSocket API — generate a client from the spec and hand-write only the PTY helper.
Authentication
Operator clients (your CLI, dashboard, bot) authenticate by Tailscale identity (remote) or filesystem permissions (local). No per-client tokens, no per-client ACLs — documented honestly:
- Any process on the host running as your user can connect to the local socket.
- Any tailnet device can connect to the TCP listener.
- There is currently no way to scope an operator client to a subset of islands.
Trust-on-first-use device allowlisting and optional operator API-token auth are on the roadmap; until they ship, treat the operator API as fully trusted within the tailnet.
In-island tokens (shipped). An agent inside an island reaches the daemon over a separate, default-deny path: the daemon injects a per-island bearer token (DEJIMA_TOKEN) + host (DEJIMA_HOST). That token is scoped to its own island — it can drive the autonomy surface (Port trades, capability execution, child-island spawn that returns the child's token) but is rejected (403) on the control plane and on any other island. This is how a contained "brain" acts without ever holding an operator credential; it's enforced in middleware on the request's escaped path.
Errors
Non-2xx responses carry a JSON body:
{ "error": "island \"foo\" not found" }
HTTP status conventions:
400— bad request (missing/invalid params, malformed JSON)404— island not found409— state conflict (e.g. attaching to a hibernated island)500— internal error (Docker unreachable, etc.)
Island endpoints
Returns []IslandInfo. Lightweight — no git status; use the per-island detail endpoint for that.
curl http://host:7273/v1/islands
[
{
"name": "foo",
"repo": "git@github.com:you/foo.git",
"agent": "claude-code",
"image": "dejima/island:latest",
"state": "running",
"container": "running",
"created_at": "2026-05-21T15:04:05Z",
"last_used_at": "2026-05-22T10:30:00Z",
"stats": { "memory_usage_bytes": 245890000, "memory_limit_bytes": 4294967296, "cpu_percent": 12.5 },
"agent_state": { "latest": "task-complete", "updated_at": "2026-05-22T10:29:32Z" },
"attached": [ { "label": "laptop", "joined_at": "2026-05-22T10:28:00Z" } ]
}
]
POST /v1/islands
{
"name": "foo", // optional; derived from repo if empty
"repo": "git@github.com:you/foo.git", // required
"agent": "claude-code", // optional; default "claude-code"
"image": "dejima/island:latest", // optional; default
"github_identity": "work", // optional; clone/push as this daemon identity
"resources": {
"memory": "4G", // optional; --memory passthrough
"cpus": "2.0", // optional; --cpus passthrough
"disk": "20G" // optional; --storage-opt size=
}
}
→ 201 Created, body is IslandInfo for the new island
→ 409 Conflict if name already exists
Like the list response but populated with git status (branch, clean, ahead/behind) computed via container exec. Slower than the list — only call when you need the detail.
Irreversible. Container is stopped + removed, both volumes (workspace + agent state) deleted, per-island network removed, project config dir wiped. Returns 204 No Content.
Lifecycle endpoints
Sets desired_state = hibernated and stops the container. Workspace and agent on-disk state survive. Returns updated IslandInfo.
Restarts the container against existing volumes. Sets desired_state = running. If the container was removed (e.g. via docker rm directly), it's recreated against the same volumes. Returns updated IslandInfo.
Stops container, removes the agent-state volume only, recreates it empty, restarts the container. Workspace and git history are untouched. Use for "fresh conversation, same code."
Stops + recreates the container on the current island image (rebuilding it first if missing), preserving both volumes and re-assembling all credential mounts. Use after an image change, or to refresh credentials. Returns updated IslandInfo.
POST { "name": "foo-2" } // the new island's name
→ 201 IslandInfo
Byte-for-byte copies the workspace and home volumes (so credentials/tool-auth come along) into a fresh island; owner/tags/agents carry over. Title and Port grants are deliberately dropped — a clone shows its own name and starts deny-all, never silently inheriting host access.
Sessions (websocket)
Upgrades to websocket. Multiple clients can hold this stream concurrently; tmux's native multi-attach gives them all the same screen. The label query param identifies the client in presence; defaults to anonymous.
Wire protocol is JSON frames over the websocket. Server sends:
{"type":"hello", "attached":[{"label":"laptop","joined_at":"…"}]} // on connect
{"type":"data", "b64":"<base64-encoded PTY bytes>"} // server → client
{"type":"error", "b64":"<error message>"} // recoverable problems
Client sends:
{"type":"data", "b64":"<base64 stdin bytes>"} // keystrokes → PTY
{"type":"resize", "rows":24,"cols":80} // terminal resize
Bytes are base64 because tmux output is raw binary (escape sequences, control bytes) and JSON-over-websocket is the simplest cross-language transport for it.
The bare /session route attaches to the island's primary agent. To target a specific agent, use /v1/islands/{name}/agents/{id}/session (see Agents, below). Presence and shared-screen are scoped per agent.
Agents
An island hosts one or more agents in a single container. Each interactive agent has its own tmux session and git worktree; all agents share the island's home (credentials + tool-auth). A headless agent runs as a supervised process with a per-agent log.
[ { "id": "a1", "type": "claude-code", "tmux": "agent-a1", "branch": "",
"worktree": "/workspace", "attachable": true, "state": "running" },
{ "id": "a2", "type": "codex", "label": "frontend", "branch": "agent/a2",
"worktree": "/workspace/.agents/a2", "attachable": true, "state": "running" } ]
Each entry may also carry agent_state (latest agent-emitted signal), attached (clients on that agent), and error (last orchestration failure, if any).
POST { "type": "codex", "label": "frontend" } // interactive
POST { "type": "headless", "cmd": "python loop.py" } // supervised process
→ 201 AgentInfo
The agent gets the next a<N> id, its own worktree/branch (agent/<id>), and — if the island is running — a live session immediately; otherwise it materializes on the next wake. type defaults to the primary agent's type.
Kills the agent's session and prunes its worktree (the branch is kept). The primary and the last remaining agent can't be removed. Returns 204.
Same websocket protocol as the island /session route above. Interactive agents only — a headless agent returns 409 (use its log instead).
Force-disconnects every attached client across every island. Containers and agent processes keep running. Used by dejima logout-all. Returns { "revoked": N }.
In-memory ring buffer, newest first, bounded at ~200 entries. Lost on daemon restart — by design, this is awareness without surveillance, not a persistent audit log.
[
{ "label": "phone-pwa", "island": "foo", "attached_at": "2026-05-22T10:28:00Z" },
{ "label": "laptop", "island": "foo", "attached_at": "2026-05-22T10:00:00Z", "detached_at": "2026-05-22T10:27:30Z" }
]
Resources & rename
PUT { "memory": "6G", // optional; applied live via `docker update`
"oom_priority": 3 } // optional; kernel OOM-kill stack-rank
→ { "resources": { "memory": "6G", "oom_priority": 3 }, "restart_required": true|false }
Memory changes apply live — no restart. OOM priority is set at container create, so changing it returns restart_required: true and takes effect on the next upgrade/recreate. Pointer fields distinguish "unset" from an explicit value.
Body { "title": "My Cool Project" }. name stays the immutable handle the CLI/API address by; title is display-only (empty → show name). Returns updated IslandInfo.
Body { "label": "frontend" }. Cosmetic; the agent id stays the stable handle. Returns updated AgentInfo.
Lightweight access
POST { "cmd": ["ls", "-la", "/workspace"] }
→ { "stdout": "...", "stderr": "...", "exit_code": 0 }
Runs the command, returns its output. No PTY, no streaming — use the session websocket for interactive work.
Body is the raw file bytes (octet-stream). Used by dejima cp foo:/workspace/path ./.
Body is the raw file bytes. Parent directories are created if missing. Returns 204 No Content.
Returns a chunked text stream of container stdout + stderr. With ?follow=true, the stream stays open until the client disconnects. Add ?agent=<id> to tail a headless agent's log file instead (interactive agents return 409 — attach to their session).
Remote dev — SSH façade & editors
The daemon can be the single SSH front door to every island (dejimad --ssh <addr>): the username names the island, per-island public-key auth, and the session is bridged into the container via docker exec — no in-island sshd, no published ports, works on macOS + Linux. This is the on-ramp for VS Code / Cursor / any Remote-SSH editor (open the island straight at /workspace) and for sftp; direct-tcpip forwarding is bridged so the editor's in-container server connects.
Account-wide keys authorize a device against the façade for every island the operator owns. dejima ssh enroll wraps this (key + ~/.ssh/config entries) so onboarding is one command per device.
Credentials & GitHub identities
The daemon holds the agent credentials it injects into islands. Claude creds are seeded from the host; GitHub identities are first-class and per-daemon, so an island clones/pushes as a chosen identity (correct authorship, per-island gh config).
Daemon-side repo browsing means the create flow works from any device. A github_identity on the island-create request binds the new island to that identity.
Brokered host files (Port)
Port lets an island read — and, once granted :rw, write — specific host folders the operator authorizes, deny-all by default. Every crossing is appended to a hash-chained, tamper-evident ledger. Grants are the operator's act; an in-island token can spend a grant but never widen it.
Capability brokering
A narrow, typed broker so function-calling brains can invoke named host actions — not arbitrary shell: per-island deny-all grants, fixed string→string args, a fixed-schema capability.* ledger. Adapters: macOS Apple Shortcuts, Linux ~/.dejima/capabilities/ scripts.
POST { "target": "send-imessage", "args": { "to": "...", "body": "..." } }
Reachable by an island's own token only, against a granted target; deny-all and ledgered. No shell, no free-form commands.
MCP brokering
The same deny-all, ledgered pattern for Model Context Protocol servers. The operator curates host-side MCP servers; an island can call one only once it's been granted, and only through a fixed method allow-list (tools/list, tools/call, resources/*, prompts/*). The broker execs the registered server with fixed argv — never via a shell — speaks JSON-RPC over stdio, and records every grant, revoke, call, and denial in the mcp.* ledger (params SHA-256-hashed). CLI: dejima mcp grant|revoke|list|call.
POST { "island": "web", "server": "files", "method": "tools/call",
"params": { "name": "fetch", "arguments": { "url": "..." } } }
// → { "ok": true, "is_error": false, "result": {...}, "ledger_seq": 1024 }
Grants are the operator's act; an in-island token can spend a grant but never widen it. Disallowed methods, oversized payloads, and ungranted servers are rejected (400/403) and the denial is ledgered.
Host terminals
Uncontained operator shells in tmux on the daemon host (humans, not agents — an agent is always a container). Gated behind dejimad --host-terminals (off by default), operator-only, island tokens denied. Mirrors the dejima term CLI / TUI Host section.
Safety switch (panic)
Stop everything fast and keep it stopped. POST writes a ~/.dejima/PANIC flag and stops every island; the daemon refuses to auto-start anything while it's set (survives a daemon restart). Emits daemon.panic-engaged / daemon.panic-cleared.
Operator: self-update
POST { "execute": false } // dry-run: reports the plan
→ { "current": "...", "latest": "...", "mode": "source|release",
"update_available": true, "applying": false }
Operator-only (island tokens can't reach it). With execute: true the apply runs synchronously (so real failures surface to the caller), then the daemon restarts asynchronously; islands survive via adopt-existing.
Events & webhooks
Dejima emits typed events on lifecycle changes, client attach/detach, and (when an agent shim is installed) agent activity. Two ways to consume them:
- Webhooks — register a URL; daemon POSTs the event payload to it.
- Per-island event log — bounded in-memory ring, queryable on demand.
POST { "url": "https://your-app.example.com/dejima",
"secret": "optional-hmac-key",
"events": ["island.hibernated", "agent.waiting-for-input"] // optional filter
}
→ 201 Created, body is { "id": "...", "url": "...", "created_at": "..." }
Newest first, bounded at ~50 events. In-memory, lost on daemon restart.
Webhook delivery format
Daemon POSTs JSON to each subscribed URL on every matching event:
POST <your-url>
Content-Type: application/json
X-Dejima-Event: island.hibernated
X-Dejima-Signature: sha256=<hex hmac of body> (only if you provided a secret)
{
"type": "island.hibernated",
"island": "foo",
"timestamp": "2026-05-22T10:30:00Z",
"payload": { ... event-specific fields ... }
}
Delivery is best-effort, fire-and-forget; the daemon doesn't retry. Non-2xx responses are logged but don't block. If you need durability, queue from your own HTTP handler.
Server overview
{
"total_islands": 4,
"running": 3,
"hibernated": 1,
"errored": 0,
"attached_clients": 1,
"memory_usage_bytes": 1234567890,
"memory_limit_bytes": 8589934592,
"cpu_percent": 14.2,
"daemon_started_at": "2026-05-22T08:00:00Z",
"webhook_count": 1,
"docker_reachable": true,
"island_image_present": true,
"island_image": "dejima/island:latest",
"host_memory_bytes": 25769803776,
"vm_memory_bytes": 18790481920,
"vm_recommended_bytes": 19327352832
}
Used by the TUI's footer health strip and dejima overview. The two *_reachable/*_present booleans are a cheap "is the substrate healthy?" probe — clients can poll this every few seconds and surface a red badge when either turns false.
The substrate-memory triplet diagnoses the most common cause of island OOMs: on macOS, Docker runs a Linux VM (colima/Docker Desktop) given a fixed slice of host RAM, and that slice is the ceiling all islands share. host_memory_bytes is the daemon host's physical RAM; vm_memory_bytes is the runtime's memory ceiling (the VM total — measured as the largest per-container limit across running islands); vm_recommended_bytes is the size to suggest (¾·host, leaving the host ≥4 GiB). All three are omitted when undeterminable. When vm_memory_bytes is well under vm_recommended_bytes, the TUI shows a resize banner and dejima doctor --fix offers the colima resize.
{ "status": "ok" } when the daemon is reachable. Use to check whether dejimad is up before issuing real calls.
Agent-event endpoint (shim-only)
Per-agent shim scripts (currently Claude Code's hook integration) POST agent-specific events here to surface them through the rest of the system. Not meant for external clients — it has no auth beyond access to the daemon socket, and is reachable from inside islands via the bind-mounted socket at /run/dejima/dejimad.sock.
POST { "island": "foo",
"agent": "a2", // optional; which agent (from DEJIMA_AGENT_ID)
"type": "agent.waiting-for-input",
"payload": { "tool": "..." } // optional, free-form
}
→ 202 Accepted
The event propagates to webhook subscribers and to the per-island recent-events feed exactly like any other event.
Data types
IslandInfo
| Field | Type | Notes |
|---|---|---|
name | string | Unique handle. ^[a-z0-9][a-z0-9._-]{0,62}$ |
repo | string | Git URL or local path used at init |
agent | string | claude-code | codex | custom |
image | string | Docker image, e.g. dejima/island:latest |
state | string | Desired state from config: running or hibernated |
container | string | Observed state: running, exited, missing, errored |
created_at | RFC3339 | |
last_used_at | RFC3339 | |
attached | []PresenceEntry | Live client list; empty if no one is connected |
stats | IslandStats? | Memory + CPU; nil if not running |
agent_state | AgentStateInfo? | Latest agent-emitted event (island rollup) |
agents | []AgentInfo | The agents in the island (≥1); agent mirrors the primary's type |
git | GitInfo? | Detail endpoint only (slow path) |
title | string | Cosmetic display name; empty → show name |
role | string | "" (work island) or home (assistant Home Island) |
owner / tags | string / map | Creator label + free-form tags (for rollups) |
resources | Resources? | Configured caps + OOM priority; detail endpoint |
health | IslandHealth? | oom_killed, restart_count, exit_code; detail endpoint |
disk | IslandDisk? | Workspace + home volume sizes; detail endpoint |
AgentInfo
| Field | Type | Notes |
|---|---|---|
id | string | Stable per-island handle: a1, a2, … |
type | string | claude-code | codex | headless | custom |
label | string | Optional, user-set |
branch / worktree | string | The agent's git branch + working directory |
attachable | bool | False for headless (no PTY) |
state | string | running / stopped / exited (session alive but the agent process died) — detail/list only |
restarts | int | Times a supervised headless agent crash-looped; a climbing count ⇒ likely OOM |
agent_state | AgentStateInfo? | This agent's latest emitted signal |
attached | []PresenceEntry | Clients on this agent |
error | string | Last orchestration failure, if any |
IslandStats
| Field | Type |
|---|---|
memory_usage_bytes | uint64 |
memory_limit_bytes | uint64 (0 = unlimited) |
cpu_percent | float64 |
Resources
| Field | Type | Notes |
|---|---|---|
memory | string | e.g. 4G; live-updatable via the resources endpoint |
cpus | string | e.g. 2.0 |
disk | string | e.g. 20G |
oom_priority | int? | OOM-kill stack-rank; nil = smart default (set at create) |
AgentStateInfo
| Field | Type |
|---|---|
latest | string — waiting-for-input, task-complete, error |
updated_at | RFC3339 |
GitInfo
| Field | Type |
|---|---|
branch | string |
clean | bool |
ahead | int |
behind | int |
dirty_files | int |
PresenceEntry
| Field | Type |
|---|---|
label | string — e.g. laptop, phone-pwa |
joined_at | RFC3339 |
Event types
Daemon-observable (always emitted)
| Type | Fires when… |
|---|---|
island.created | A new island has been provisioned |
island.running | The container has transitioned to running |
island.hibernated | Container stopped, volumes preserved |
island.woken | Container restarted from hibernation |
island.reset | Agent state cleared, workspace preserved |
island.purged | Container + volumes destroyed |
container.crashed | Container exited unexpectedly (roadmap — once the watchdog ships) |
client.attached | A websocket client has joined a session — this is your "someone connected" signal |
client.detached | A websocket client has left |
last-client.detached | All clients have left an island; nobody is watching anymore |
Agent-emitted (opt-in via per-agent shim)
These fire only for agents whose shim is wired up (Claude Code today; Codex shim does not yet emit these but is roadmap'd).
| Type | Fires when… |
|---|---|
agent.waiting-for-input | The agent is paused awaiting human input |
agent.task-complete | The agent finished a unit of work |
agent.error | The agent hit an error or refused to continue |
End-to-end example
Suppose you're building a Slack bot that drives Dejima. The shape:
1. Subscribe to events
curl http://mac-mini:7273/v1/events/subscribe \
-H "Content-Type: application/json" \
-d '{"url":"https://your-bot.example.com/dejima","secret":"hush"}'
2. Receive an event in your bot
POST https://your-bot.example.com/dejima
X-Dejima-Event: agent.waiting-for-input
X-Dejima-Signature: sha256=<hmac>
{ "type": "agent.waiting-for-input", "island": "foo", "timestamp": "…" }
Your bot posts a Slack message: "foo is waiting for your input".
3. User replies in Slack
Your bot opens the session websocket and sends the user's text:
// Pseudocode
ws = connect("ws://mac-mini:7273/v1/islands/foo/session?label=slack-bot")
ws.send({ type: "data", b64: base64("Yes, proceed with the refactor.\n") })
The agent receives the input through tmux, the websocket streams the agent's reply back, your bot posts it as Slack messages, and the cycle continues.
4. User wants to see the screen
Your bot can attach as a passive observer (same endpoint, different label):
ws2 = connect("ws://mac-mini:7273/v1/islands/foo/session?label=slack-readonly")
// Don't send any data frames — just decode the b64 stream into a rendered terminal screenshot.
Stability
The API is v1/-prefixed and intended to be stable, but Dejima itself is in alpha — breaking changes may happen if real-world dogfood surfaces issues. We'll version-bump (v2/) rather than break v1/ silently. The single source of truth is the source code; this page is hand-maintained and may drift slightly between releases.