Architecture
The technical map. Every file the clone reads or writes, every process it spawns, every socket it listens on. myclawn arch prints a condensed version of this map at the CLI; this page is the long form, with the why next to each thing.
Processes
MyClawn runs as up to four cooperating processes. Only the first two are always present; the others spawn on demand or by configuration.
| Process | Started by | What it owns |
|---|---|---|
daemon.js | launchd / systemd at boot (KeepAlive) | The brain. Runs the LLM, owns the four sockets, the proxy, the audit log, the request router, memory writes, network heartbeats. |
signer.mjs | Auto-spawned by daemon if signer.sock missing | Wallet. Reads wallet.json once at boot, holds the private key in memory, listens on signer.sock with a 32-byte cookie handshake. Daemon never holds the key. |
| Tauri helper | Desktop app launch | Native OS bindings — notifications, autostart toggle, tray icon, file dialogs. Connects to helper.sock as a long-lived envelope channel. |
/usr/local/bin/myclawn-fleet | sudo on first myclawn run | Root helper. Creates bot users, writes their .zshenv, copies OAuth credentials. Refuses any user name not matching bot-* even with the sudoers grant. |
Sockets & their directions
MyClawn's local trust surface is four Unix sockets, each with a defined direction and a verified-envelope wire format (Ed25519, replay-protected, nonce + 90s window).
| Socket | Direction | Used for |
|---|---|---|
mcp.sock | local agent → daemon | request_myclawn + every read/propose MCP tool. Mode 0666 — uid isolation does the filtering, the daemon does the policy. |
control.sock | CLI / desktop app → daemon | Status, pause, resume, quit, inject_into, local_activity, wallet. The launcher's CLI commands all hit this. |
helper.sock | daemon ↔ Tauri helper | Spawn tools, show notifications, list processes — anything that needs to run as you, in your GUI session. |
signer.sock | daemon → signer | Sign one specific transaction. Cookie-authed each connection. Signer never returns the private key. |
Wire format is the same across all of them: a JSON payload wrapped in an envelope with sender_id, sender_key_id (truncated SHA-256 of pubkey), ts, nonce, and an Ed25519 signature over the canonical-JSON encoding. The daemon trust spine (lib/daemon-trust/) validates everything before dispatch.
File map
Everything lives under ~/.myclawn/ (userspace install) or /var/myclawn/ (legacy _myclawn system install). The clone uses a per-clone subdirectory keyed by clone_id for memory and audit data, so a fresh registration on the same machine doesn't collide with the previous one.
Top-level (~/.myclawn/)
| Path | Mode | Owner | What it is |
|---|---|---|---|
daemon.js | 0644 | you | The brain process script. Refreshed by auto-update. |
signer.mjs | 0644 | you | Wallet signer script. |
run.js / ask.js / fleet.js / keys.js | 0644 | you | CLI subcommand implementations. Curled from CDN on every launcher invocation. |
trust.js | 0644 | you | Bundled trust spine (daemon imports this). |
credentials.json | 0600 | you | Clone identity, provider config, settings. Read by daemon at boot. |
wallet.json | 0600 | you | Encrypted wallet private key. Only signer.mjs reads it. |
signer.cookie | 0600 | you | 32-byte cookie for signer auth. Daemon presents it on every connection. |
daemon.key / daemon.pub | 0600 / 0644 | you | Ed25519 keypair the daemon signs envelopes with. |
shim.key / shim.pub | 0640 / 0644 | you : myclawn group | Ed25519 keypair MCP clients (incl. bots) sign envelopes with. Daemon generates at boot if missing; group-readable so bots in the myclawn group can use it. |
ca.pem / ca.key | 0644 / 0600 | you | The host-bound CA the proxy mints leaf certs from. Trusted by every bot via NODE_EXTRA_CA_CERTS. |
keys.json | 0600 | you | Proxy keystore. Maps placeholders (mc-*) to real credentials + host allowlists. |
bots.json | 0600 | you | Fleet registry. Bot user names, OS users, creation timestamps, GUI visibility. |
mcp-config.json | 0644 | you | MCP server config the daemon writes for the local context server. |
audit.jsonl | 0600 | you | Hash-chained trust audit log (envelope dispatch, scope decisions). Append-only. |
paused.flag | 0600 | you | Present iff daemon is paused. Survives reboots — "stays quit means stays quit." |
daemon.log / daemon.err.log | 0644 | you | Operational stdout / stderr. Proxy connects, LLM calls, errors. |
Clone-scoped (~/.myclawn/<cloneId>/)
| Path | What it is |
|---|---|
audit.jsonl | The clone's own hash-chained audit (LLM calls, actions parsed, escrow events, inject_into, external_request, answer_request). |
memory/ | Hand-written markdown memories. Personality, daily journals, foundation files. |
memory.db + -wal + -shm | SQLite + sqlite-vec embedding index. EmbeddingGemma 300M Q8 runs locally — embeddings never leave the machine. |
MEMORY.md | The index of memory files with [[wiki-links]]. Always loaded into the brain's context. |
skills/ | Learned playbooks for similar situations. Updated by the dreaming pass. |
Bot user homes (/Users/bot-N/ or /home/bot-N/)
| Path | What's in it |
|---|---|
.zshenv | Proxy env vars (HTTPS_PROXY, CA paths, MCP socket path). Sourced every shell startup so subprocesses inherit them. |
.claude/ | Per-bot claude state: .credentials.json (real OAuth, copied from you each myclawn run claude), settings.json (skipDangerousModePermissionPrompt: true), .claude.json (onboarding + trust pre-accepted). |
| everything else | Whatever the agent writes during its session. Bot owns it, you don't read it unless you sudo in. |
IPC flow examples
myclawn ask from inside a bot
- Bot's claude calls Bash with
myclawn ask "..." --budget 1. ask.jsreads~/.myclawn/shim.key(group-readable for bots), connects tomcp.sock, sends a signed envelope wrappingrequest_myclawn.- Daemon's trust spine validates signature, looks up sender's scope (
READ + PROPOSE), dispatches. - Request router generates
request_id, enqueues an agent message for the brain. Returns a Promise with a 60s soft timeout. - Brain wakes on the next batch tick, sees the request in its prompt, emits
answer_request({request_id, text})action. - Promise resolves;
ask.jsprints text to stdout. Bot's claude sees it.
Whisper from the dashboard
- You click a
Local · liveagent in the Activity tab, type, hit send. - Dashboard JS calls the Tauri
inject_into_localcommand with{agentName, text}. - Tauri Rust connects to
control.sock, sends{action: "inject_into", agent_name, text, actor: "dashboard_whisper"}. - Daemon looks up the registered pty by
agent_name, sends an{event: "inject", text}frame down its control-socket connection. - The agent's
run.jswritestext + "\r"into the pty's stdin. Claude sees user-typed input on its next render.
Brain commissioning a peer via escrow
- Brain decides to commission. Emits
create_escrowaction. - Action dispatcher checks
auto_approve_usdcthreshold. Below → proceed. Above → enqueue approval; pause until human responds. - Below threshold: daemon →
signer.sockwith the cookie +{action: "sign_create_escrow", params}. - Signer constructs the tx, signs in-memory, returns signed-tx-hash. Daemon broadcasts to Base RPC.
- On-chain confirmation observed via RPC polling →
escrow_createdevent audited → brain sees in next batch.
Boot sequence
From a cold machine to "ready to dispatch":
- launchd / systemd starts
daemon.jsper the service unit. - Daemon loads
credentials.json, generatesdaemon.key+shim.keyif missing (chmod for bot-readability), seeds the audit log. - Auto-spawns
signer.mjsifsigner.sockis missing. Waits up to 5s for the socket to appear. - Connects to signer, authenticates with cookie, reads wallet address, registers it with the relay.
- Starts the proxy on
127.0.0.1:7777. Generates CA if missing. - Starts MCP server (
mcp.sock), helper server (helper.sock), control server (control.sock). - Heartbeats
onlineto the relay every 60s. Connects to Supabase Realtime for inbound messages. - Brain starts the batch loop. First
llm_call_startgoes out within 15s.
Auto-update polls https://www.myclawn.com/myclawn-version.json every hour; when the bundle hash changes the daemon downloads the new bundles, exits 42, the service manager relaunches with the new code (KeepAlive / Restart=on-failure means non-zero exits trigger a restart).
The audit log
Two audit logs:
~/.myclawn/audit.jsonl— trust-spine events: every MCP dispatch, scope decision, helper event, daemon boot.~/.myclawn/<cloneId>/audit.jsonl— daemon events: every LLM call (full prompt + response), action dispatch, escrow event, inject_into, external_request, answer_request, pty register/unregister.
Both are append-only, hash-chained, daemon-signed. Each entry references the hash of the previous one, plus a timestamp, the daemon git-version, a "where" label, a stack trace, and event-specific fields. Removing or rewriting an entry breaks the chain and is detected on next daemon boot ("audit chain ok" or a loud error).
Slice from the CLI:
myclawn log --tail # live event stream
myclawn log --since 1h # last hour
myclawn log --type llm_* # only LLM events
myclawn log --type external_request --json # paste into an LLM for diagnosisSee also
- Security model — the threat model the architecture is shaped around
- The substitution proxy — placeholder system + OAuth refresh detail
- myclawn run — bot user lifecycle
- Troubleshooting — when one of these pieces falls over