@dsiloed/silo-link
v1.19.22
Published
Claude Code Remote Bridge — connects Claude Code sessions to DSiloed via MCP + ActionCable
Downloads
1,604
Maintainers
Readme
SiloLink — Claude Code Remote Bridge
SiloLink is a local daemon that connects Claude Code sessions to DSiloed conversations. It provides an MCP server that Claude Code connects to, and maintains a real-time WebSocket connection to DSiloed via ActionCable. Messages flow bidirectionally — you can launch, monitor, interact with, and command Claude Code sessions from the DSiloed web UI, Slack, SMS, or Discord.
See it in action — Interactive Demo | Documentation
┌──────────────────────┐
│ DSiloed Server │
│ │
Claude Code (tmux) ──MCP──► │ │
Claude Code (tmux) ──MCP──► SiloLink ──WebSocket──► ActionCable
Claude Code (tmux) ──MCP──► :3579 ──REST API──► ├── ConversationChannel
│ ├── ControlChannel
│ Claude Launcher │
│ Message Queue │ Web UI / Slack
│ Session Manager │ Discord / SMS
└──────────────────────┘Features
- Launch Claude sessions from the web UI — click "+" in the SiloLinks section to spawn a new session
- Auto-launch — send a message to any SiloLink conversation and Claude starts automatically
- Multi-session — each conversation gets its own Claude Code tmux session
- Message buffering — messages that arrive before Claude registers are queued and delivered
- Status notifications — "Starting session..." and "Session ready!" feedback in the conversation
- Agent Dashboard — active sessions appear in Mission Control with real-time status
- Control Channel — launch/stop/status commands via ActionCable from the UI
- Channel linking — command Claude from Slack, Discord, Teams, or SMS
- Session continuity —
remote_load_contextrestores prior conversation history on session restart - Workspace isolation — git worktrees give each session its own branch and working directory
- File claim tracking — advisory soft-locks with cross-session conflict notifications
- Provider abstraction —
AgentLauncherinterface supports Claude, Gemini, and Codex (OpenAI) adapters
Installation
npm install -g @dsiloed/silo-linkThis installs the silolink CLI globally.
Configuration
silolink configThe wizard first asks whether the daemon is running for a user or an agent and then collects only what's needed. Your answer is persisted as identity_type in the config and changes how the spawned Claude Code session is launched (see below).
- User (default): prompts for host, tenant, login (email or username) and password. SiloLink calls
POST /api/v1/users/login, captures the returned JWT, and stores it in~/.silolink/config.json(mode0600). Your password is never written to disk — only the JWT. - Agent: prompts for host, tenant, agent name (the
subclaim), and the JWT to paste in. Generate the token from the LLM Manager UI: open the agent's edit dialog, expand SiloLink Authentication, and click Generate SiloLink Token. The agent path never needs Sorcery credentials, since agents don't have a human login.
The token is long-lived. If the server ever rejects it (password change, tenant key rotation, agent rename), re-run silolink config to refresh it.
How identity_type changes session launches
The choice you make in the wizard controls where the spawned Claude Code session starts (which is the same dir CC reads .mcp.json from):
identity_type: "user"— single-env remote-bridge use case. CC launches inprojects_path(or inrepo_pathif a launch command supplies one). CC inherits anyCLAUDE.mdfrom your projects dir, and you typically runsilolink setup-claude-mdonce to inject the SiloLink protocol section into it. There's only one writer of the.mcp.jsoninprojects_path, so collisions are not a concern.identity_type: "agent"— multi-agent SDLC orchestration where several silolink envs (e.g.lead-tester,vixen) run on the same laptop. Each env gets its own per-env staging dir at~/.silolink/staging/<env>/, and CC launches there so its.mcp.json(which includes the env-specific JWT) is never shared with a sibling env. A barerepo_pathin a launch command is treated as informational; the agent's runbook tells the spawned CC where tocdfor repo work. When a launch supplies bothrepo_pathandbranch, silolink creates a per-ticket worktree under~/.silolink/worktrees/<repo>/<branch>/— also identity-isolated.
Worktree-mode (repo_path + branch) is identity-safe in either mode and works the same way for both.
If you ever migrate an env from user to agent (e.g. a personal config that's grown into an agent setup), re-run silolink config --env <name> and answer agent — the daemon will pick up the new identity_type on next restart.
Non-interactive setup
For automated installs (e.g. a prompt generated by the DSiloed UI for Claude Code to execute), skip the wizard:
silolink config --non-interactive \
--host https://www.dsiloed.com \
--tenant <tenant_id> \
--user-sub <email-or-username> \
--jwt <pre-issued-jwt> \
[--mcp-port 3579] \
[--projects-path /home/user/projects] \
[--env dev]All four of --host, --tenant, --user-sub, --jwt are required when --non-interactive is set. The JWT is written to ~/.silolink/config.json exactly as supplied — generate it via the same flow used by the user's MCP config dialog (or the agent's "Generate SiloLink Token" button).
~/.silolink/config.json schema:
| Field | Description | Default |
|-------|-------------|---------|
| host | DSiloed server URL | https://www.dsiloed.com |
| tenant_id | Tenant enterprise identifier | harmoniq |
| user_sub | Your username or email | — |
| jwt | JWT obtained at silolink config time (do not edit by hand) | — |
| mcp_port | Local port for the MCP server | 3579 |
| reconnect_interval_ms | Base reconnect delay | 5000 |
| max_reconnect_attempts | Max reconnect tries | 20 |
| agent_provider | AI provider: claude, gemini, openai | claude |
| claude_command | Path to Claude CLI binary | claude |
| claude_working_directory | Working directory for spawned sessions | — |
| claude_auto_respawn | Auto-respawn on idle/exit | false |
| claude_idle_timeout_ms | Idle threshold before respawn | 30000 |
| claude_session_prompt | Default prompt for spawned sessions | Register with SiloLink... |
| stale_inbound_grace_ms | On an inbound message, never recycle a session younger than this (a fresh pane may take minutes to register) | 600000 (10 min) |
| report_back_idle_ms | After a final:true report with no further work, complete the session (state: idle_timeout) and kill the pane; 0 disables | 120000 (2 min) |
| poll_idle_complete_ms | Opt-in. End a session parked in remote_poll with zero work calls for this long; 0 = disabled | 0 |
Example config file (after running silolink config):
{
"host": "https://www.dsiloed.com",
"tenant_id": "portablemind",
"user_sub": "[email protected]",
"jwt": "eyJhbGciOiJIUzI1NiJ9...",
"mcp_port": 3579,
"claude_command": "/home/user/.local/bin/claude",
"claude_working_directory": "/home/user/projects",
"claude_auto_respawn": true,
"claude_idle_timeout_ms": 30000
}Usage
Starting SiloLink
# Foreground (see logs in terminal)
silolink start
# Background (daemon mode)
silolink start --daemonThe daemon owns its log: every run appends (never truncates) to
~/.silolink/logs/silolink-<env>.log, with a session-start banner and an ISO-8601
timestamp on every line. Do not wrap start in | tee or redirect its output
into that same file (>> ~/.silolink/logs/silolink-<env>.log) — the daemon detects
an fd that already is the log and won't double-write, but the redirect is redundant.
Send a wrapper's stdout/stderr to /dev/null; when stdout is not a terminal the
daemon mutes console output anyway (set SILOLINK_CONSOLE=1 to keep it).
Managing the Daemon
silolink status # Check connection state and active sessions
silolink sessions # List active sessions
silolink stop # Stop daemon (kills all Claude tmux sessions)Launching Claude Sessions
# Launch from CLI
silolink launch
silolink launch -p "Work on the auth refactor"
# Launch from the DSiloed web UI
# Click "+" in the SiloLinks section of TeamChatReporting your own coding-agent usage
Post the token usage of a Claude Code / Gemini / Codex session you ran yourself, so its cost is tracked under your DSiloed user — ideal as a Stop/SessionEnd hook:
silolink report-usage --cli claude # also: gemini | codex
silolink report-usage --cli claude --dry-run # preview without postingSee docs/report-usage-hooks.md for hook setup across all three CLIs.
Connecting Claude Code (Manual)
Add SiloLink as an MCP server in your Claude Code settings (.claude.json or project .mcp.json):
{
"mcpServers": {
"silolink": {
"command": "npx",
"args": ["mcp-remote", "http://localhost:3579/mcp"]
}
}
}Claude Session Launcher
SiloLink spawns and manages Claude Code sessions using tmux:
On-Demand (from DSiloed UI)
- Click + in the SiloLinks section of TeamChat
- Enter a session name and optional prompt → Launch Session
- Conversation appears immediately — "Starting session..." shows while Claude initializes
- Claude sends "Session ready!" when the poll loop begins (~20-30 seconds)
Auto-Launch (on inbound message)
When a message arrives on a SiloLink conversation with no active session:
- SiloLink posts "Restarting session..." immediately
- Spawns Claude Code in a new tmux session
- The triggering message is buffered and delivered once Claude registers
- Claude sends "Session ready" and enters the poll loop
Multi-Session
Each conversation gets its own tmux session. Multiple sessions run simultaneously.
# List active tmux sessions
tmux list-sessions | grep silolink
# Attach to watch Claude work
tmux attach -t silolink-claude-1774363497579
# Detach: Ctrl+B, DSession Lifecycle
- Launch → SiloLink posts "Starting session..." → spawns tmux → Claude registers → "Session ready!"
- Messages → user sends message → SiloLink enqueues → Claude polls and responds
- Delete → gear icon → "Delete SiloLink" stops the Claude session and deletes the conversation
- Shutdown →
silolink stopkills all tmux sessions and completes agent sessions on DSiloed
MCP Tools
| Tool | Description | Blocking |
|------|-------------|----------|
| remote_register | Register session, create/attach conversation (binds to the launched conversation) | No |
| remote_load_context | Load prior conversation history for session continuity | No |
| remote_notify | Fire-and-forget message (accepts final?) | No |
| remote_post | Post to ANY conversation by id (accepts final?) | No |
| attach_local_file | Upload a file from the session's working tree (or attach_allowed_roots) as an LlmFile and attach it to a conversation — symlinks resolved before the containment check, 50 MB cap, bytes never pass through the model | No |
| remote_ask | Post question, wait for reply | Yes |
| remote_poll | Non-blocking check for next message (recommended) | No |
| remote_check_messages | Non-blocking check for ALL pending messages | No |
| remote_wait_for_command | Block until next message (prefer remote_poll) | Yes |
| remote_sessions | List active sessions | No |
| remote_unregister | Unregister and clean up | No |
| remote_workspace_create | Create an isolated git worktree for this session | No |
| remote_workspace_claim | Claim files (advisory soft-lock) with conflict detection | No |
| remote_workspace_check | Check file claims across all sessions | No |
| remote_workspace_merge | Merge worktree branch back and clean up | No |
| remote_workspace_list | List all active workspaces | No |
remote_register
Input: { session_name?: "my project", conversation_id?: 453 }
Output: { session_id, conversation_id, conversation_url }When conversation_id is provided, attaches to that existing conversation (used by auto-launcher).
For a session the daemon launched, the MCP transport is already bound to the
launched conversation (via the per-launch X-SiloLink-Launch header), so
remote_register adopts that conversation regardless of the conversation_id
you pass. If the two disagree, the launch binding wins and the tool result
returns a conversation_mismatch warning naming the conversation you were
actually registered against. See
docs/session-reliability.md.
remote_load_context
Input: { conversation_id?: 109, limit?: 50 }
Output: { success: true, conversation_id, resume_id, message_count, history: [...] }Fetches prior conversation history and the last claude_resume_id from agent session metadata. Call after remote_register when attaching to an existing conversation to restore session continuity.
remote_poll (Recommended)
Input: {}
Output: { success: true, message: "...", sender_name: "..." }
— or —
{ success: true, pending: true }Non-blocking. Safer than remote_wait_for_command — can't lose messages on cancellation.
remote_notify
Input: { message: "Build completed successfully", final?: false }
Output: { success: true, message_id }Pass final: true on your last completion report for a dispatch (and only
then). The post is tagged silolink_report { agent_session_id, kind: completion }
so the dispatcher can tell completion from progress, and it arms the daemon's
idle-completion path (report_back_idle_ms) to gracefully end the session once
no further work arrives. remote_post takes the same final? flag.
remote_ask
Input: { question: "Should I proceed?", timeout?: 300 }
Output: { response: "Yes", sender_name: "rholmes" }Workspace Isolation
Multiple sessions can work on the same repo without conflicts using git worktrees:
Session A: feature/auth ──► ~/.silolink/worktrees/myapp/feature/auth/
Session B: feature/api ──► ~/.silolink/worktrees/myapp/feature/api/Creating a Workspace
// Agent creates a worktree on session start
remote_workspace_create({ repo: "/home/user/myapp", branch: "feature/auth" })
// → { worktree_path: "~/.silolink/worktrees/myapp/feature/auth", branch: "feature/auth" }
// Claim files before editing (advisory — warns on conflicts, doesn't block)
remote_workspace_claim({ files: ["src/auth.ts", "src/middleware.ts"] })
// → { claimed: [...], conflicts: [] }
// Check what other sessions are working on
remote_workspace_check({})
// → { all_claims: [{ session_id: "session-B", files: ["src/api.ts"] }] }
// When done, merge back
remote_workspace_merge({})
// → { success: true, merged_branch: "feature/auth" }Cross-Session Conflict Notifications
When session A claims files that session B already owns, both sessions receive automatic notifications via their message queues and DSiloed conversations.
Launcher Integration
The control channel launch command supports workspace params:
{ "type": "launch", "conversation_id": 123, "repo_path": "/home/user/myapp", "branch": "feature/auth" }Provider Selection
Set agent_provider in ~/.silolink/config.json:
{ "agent_provider": "claude" }Supported providers: claude (default), gemini, and codex (the OpenAI CLI;
openai is accepted as an alias). Launch a specific provider with
silolink launch -a codex, or per-session from the DSiloed UI.
Codex notes
The Codex launcher uses codex's native streamable-HTTP MCP support — it
writes [mcp_servers.*] tables (url + http_headers = { Authorization =
"Bearer <jwt>" } for the dsiloed server) to $CODEX_HOME/config.toml, with no
npx mcp-remote bridge. The JWT lives in the per-env config on disk (like the
claude/gemini configs), not in argv. CODEX_HOME points at a per-env dir (each
daemon identity owns its own config) and the user's ~/.codex/auth.json login is
copied in. Codex is launched as env CODEX_HOME=<home> codex … because tmux does
not propagate caller env to panes on an already-running server (a bare codex
would read the empty default ~/.codex and expose no MCP tools). Codex reads
agent instructions from AGENTS.md — seed the SiloLink poll-loop section with
silolink setup-claude-md ./AGENTS.md. MCP tools are named <server>__<tool>
(e.g. silolink__remote_register).
Validated against codex-cli 0.141.0. Knobs (all overridable):
| Config key | Default | Purpose |
|---|---|---|
| codex_command | codex | CLI binary |
| codex_args | ["--dangerously-bypass-approvals-and-sandbox"] | fresh-launch flags (auto-approve, no sandbox prompt) |
| codex_resume_enabled | false | use codex resume <id> on respawn (off → fresh session, context restored via remote_load_context) |
| codex_session_prompt | — | override the launch prompt |
| codex_auto_respawn / codex_idle_timeout_ms / codex_idle_kill_after_ms | as Claude/Gemini | lifecycle |
If a flag changes in a future codex version, the daemon's pane watcher surfaces
error: unknown option … — adjust codex_args. The TUI ready-screen text in
getReadyIndicator() is best-effort (a miss just falls back to the ready
timeout, not a wedge).
Control Channel API
SiloLink subscribes to a tenant-scoped ActionCable control channel on startup. The DSiloed UI sends commands via REST:
| Endpoint | Method | Description |
|----------|--------|-------------|
| /api/v1/silolink/launch | POST | Launch new session (creates conversation) |
| /api/v1/silolink/stop | POST | Stop session (by conversation_id or all) |
| /api/v1/silolink/status | GET | Request SiloLink status |
# Launch
curl -X POST /api/v1/silolink/launch \
-d '{"name": "my-feature", "prompt": "Work on auth"}'
# Response
{"success": true, "conversation_id": 458, "message": "Launch command sent"}CLAUDE.md Integration
Add to your project's CLAUDE.md:
## Remote Communication (SiloLink)
When the SiloLink MCP server is connected:
1. Register with `remote_register({ session_name: "<name>" })`.
If a conversation_id is provided, pass it too.
2. **Session Continuity**: If you registered with an existing conversation_id,
call `remote_load_context()` to load prior conversation history.
3. ALL communication MUST go through SiloLink — never write to terminal.
4. Send progress updates with `remote_notify()`.
5. When idle, use the poll loop:
- Call `remote_poll()` — returns instantly
- If `{ pending: true }`: sleep 3s, poll again
- If message received: process it, notify result, resume
6. Use `remote_ask()` for questions needing immediate reply.Poll Loop Pattern
// Register (use conversation_id if provided in launch prompt)
remote_register({ session_name: "portablemind", conversation_id: 123 })
// If attaching to existing conversation, load prior context
context = remote_load_context()
// Review context.history to understand what was discussed
// Poll loop
while (idle) {
result = remote_poll()
if (result.pending) { sleep(3s); continue }
handle(result.message)
remote_notify({ message: "Done: ..." })
}Agent Sessions
Active SiloLink sessions appear in the Mission Control Agent Dashboard with:
- Session name (matches conversation name)
- Status: idle, working, waiting for human, error, completed
- Heartbeats on every tool call
- Conversation link for quick navigation
Session Reliability & Health
The daemon keeps a launched worker bound to the conversation it was launched for, judges the pane (not MCP silence) before recycling, ends parked workers so the backend learns they're done, and reports its own health on every heartbeat. In brief:
- Launch↔transport binding — each launch writes a per-launch MCP config
(
<cwd>/.silolink-mcp/conv<id>-<nonce>.json,0600) carrying anX-SiloLink-Launchheader;remote_registeradopts the launched conversation and overrides a mismatchedconversation_idwith a clear warning. - Minted session id — fresh Claude launches pass
--session-id <uuid>(never with--resume/--continue), so a shared cwd can't cross-attribute a transcript. - Stale-on-inbound recycler — recycles only a pane that exists, isn't
actively thinking, and is frozen ≥ 15 min; skips launch-pending sessions and
anything younger than
stale_inbound_grace_ms(10 min). - Idle completion — a
final:truereport followed byreport_back_idle_ms(2 min) of quiet completes the session (state: idle_timeout) then kills the pane — but only if the pane isn't actively working.poll_idle_complete_msis opt-in (default 0). - Report markers —
remote_notify/remote_postacceptfinal?and stampsilolink_report { agent_session_id, kind }on the post. - Health heartbeat — every heartbeat carries a redacted
healthblock plus an hourly[HEALTH]log line (credentials, JWT expiry, backend 403 streaks, usage-ledger depth, updater/launch state). - 403 handling — a 3-streak of 403s on
report_usage/launch_contextis terminal for retries (ledger kept, one probe every 30 min; recovers on the next 2xx). - Auto-update integrity — a hub
expected_integritypin is verified against thenpm packed tarball withssribefore installing (--ignore-scripts).
Full details, rationale, and the config keys are in docs/session-reliability.md.
Permissions
SiloLink-spawned sessions run headlessly — no human at the keyboard. Permission prompts hang the pane, so the launcher pre-clears two gates so --dangerously-skip-permissions actually takes effect on every command (the flag alone is not enough on CC 2.x).
Pre-cleared automatically on each launch:
- Workspace trust dialog (per cwd) — written to
~/.claude.jsonunderprojects.<cwd>.hasTrustDialogAccepted. - Bypass-permissions consent (user scope) — written to
~/.claude/settings.jsonas a top-levelskipDangerousModePermissionPrompt: true.
The second one is the gotcha: without that consent on file, CC still re-prompts on the "dangerous pattern" class (rm -rf, sudo, ...) even when launched with --dangerously-skip-permissions and even when the in-session footer shows ⏵⏵ bypass permissions on. The footer just reflects mode; the consent is the gate. CC's internal check (Sd() in the binary) reads this key from userSettings / localSettings / flagSettings / policySettings — all at the top level of the settings object, not nested under permissions. The schemastore claude-code-settings schema doesn't document this key as of CC 2.1.x; the binary is authoritative.
Both writes are idempotent and only touch the specific keys involved — other settings (theme, plugins, permissions block, project MCP enablement, etc.) are preserved by a shallow merge.
Manual verification:
# Should print: True
python3 -c 'import json; print(json.load(open("'$HOME'/.claude/settings.json")).get("skipDangerousModePermissionPrompt"))'If the value is missing or false, restart silolink — ClaudeLauncher.ensureBypassPermissionsConsent() runs on each session spawn and will write it.
What still prompts: CC has a hardcoded circuit breaker for catastrophic patterns (rm -rf /, rm -rf ~, etc.) that re-prompts regardless of settings. Those need a human response — see the send_keys control command for driving the prompt from the web UI.
For sessions launched outside silolink — if you're running claude by hand in a terminal:
- Option A — Skip all prompts:
claude --dangerously-skip-permissions. Requires the user-scope consent above (runsilolink startonce and it's set, or write the file manually). - Option B — Allow SiloLink tools only:
{ "permissions": { "allow": ["mcp__silolink__*"] } }in.claude/settings.json.
Technical Details
- Authentication: HS256 JWTs (24h validity, auto-refresh every 12h)
- Reconnection: Exponential backoff (1s → 60s cap)
- Echo Prevention: Tracked outbound message IDs + prefix matching
- Session Cleanup: Idle sessions cleaned after 1 hour, agent sessions completed on DSiloed
- Multi-Session: Multiple tmux sessions, one per conversation
- Message Buffering: Pre-registration messages delivered on session register
- API Timeouts: 30-second abort on all DSiloed API calls
- Map Validation: Session manager validates 3-map consistency every 5 minutes
- Shutdown: Kills all Claude tmux sessions first, then completes agent sessions
Health Check
curl http://localhost:3579/health
# { "status": "ok", "sessions": 2, "cable": "connected" }Development
pnpm dev start # Dev mode (no build)
pnpm exec tsc --noEmit # Type check
pnpm build # Build
pnpm test # TestsProject Structure
src/
index.ts # CLI entry point
cli/
commands.ts # CLI commands (start, stop, status, launch, config)
daemon.ts # PID file management
config/
config-manager.ts # ~/.silolink/config.json management
jwt-generator.ts # HS256 JWT generation + auto-refresh
core/
bridge.ts # Main orchestrator — startup, shutdown, control channel
session-manager.ts # Session registry with 3-map lookup + validation
message-queue.ts # Per-session inbound queue with acknowledgment protocol
agent-launcher.ts # AgentLauncher interface and types
base-tmux-launcher.ts # Shared tmux launcher base class
claude-launcher.ts # Claude Code launcher (tmux-based)
gemini-launcher.ts # Gemini CLI launcher (tmux-based)
codex-launcher.ts # Codex (OpenAI) CLI launcher (tmux-based)
transcript-usage.ts # Per-model token parsing (claude/gemini/codex)
report-usage.ts # End-user usage collect + post (Stop hooks)
launcher-factory.ts # Provider selection factory
workspace-manager.ts # Git worktree lifecycle, file claims, conflict detection
cable/
action-cable-client.ts # ActionCable WebSocket client with auto-reconnect
subscription-manager.ts # Conversation subscriptions, echo prevention, message buffering
api/
dsiloed-client.ts # REST client for DSiloed API (30s timeouts)
mcp/
server.ts # MCP server (Streamable HTTP on Express)
tools/
register-tools.ts # All 14 MCP tool definitions
types/
index.ts # Shared TypeScript interfacesLicense
MIT
