Skip to content

Concepts

Architecture

Sigil is one long-lived process per machine, and a set of thin clients that talk to it.

Claude Code hooks ─┐
sigil CLI ├── Unix socket ──┐
MCP stdio server ─┘ │
├──► sigil daemon ──► your database
MCP HTTP clients ──┐ │ ├ connection pool
the dashboard ├── HTTP :7777 ───┘ ├ embedding cache
│ ├ write queue
paired devices ────┴── Iroh ────────────────┤ RPC registry
└ Iroh endpoint

A hook has to be fast, because it runs on every prompt you type.

Cold-starting Node, opening a Postgres connection, loading the embedding client, and running a search costs 200–400ms. Doing that per prompt is something you’d feel on every single interaction.

The daemon keeps all of it warm. Hooks and CLI calls become a Unix socket round trip against an already-connected process, which is why search is 33ms rather than 300.

It also solves three things a per-invocation model can’t:

  • One writer. A write queue serializes mutations, which matters enormously for the embedded PGlite database and still matters for Postgres under a burst of parallel ingests.
  • Shared caches. The embedding cache is worthless if every process starts empty.
  • One peer endpoint. Iroh needs a stable identity and a live endpoint; you can’t boot one per CLI call.
Terminal window
sigil daemon status
sigil daemon start [--foreground]
sigil daemon stop
sigil daemon restart
sigil daemon logs [--follow]
sigil daemon open # the dashboard
sigil daemon url # its URL, with auth token

It auto-starts on first use. You only need these for explicit lifecycle control.

Four ways in, all reaching the same RPC registry.

TransportAddressUsed by
Unix socket~/.sigil/sockHooks, CLI, the stdio MCP server
HTTP127.0.0.1:7777Dashboard, HTTP MCP clients
MCP stdiospawned per clientMCP clients that prefer stdio
Irohpeer-to-peerPaired devices

HTTP is loopback-only by default and gated by a bearer token at ~/.sigil/gui.token. Change http.host only if you know what you’re exposing.

Sigil’s MCP tools are registered once, in a side-effect-free builder, and mounted by two different entry points:

  • stdio (src/mcp/server.js) — spawned per client. Registered through a stable shim at ~/.sigil/bin/sigil-mcp.
  • HTTP (/mcp on the daemon) — one long-lived transport, bearer token, URL that never changes.
Terminal window
sigil register # stdio launcher (default)
sigil register --http # the daemon's HTTP transport
sigil register --print # print config, write nothing

HTTP is the more portable option for clients that support it — the URL survives Node version switches and reinstalls without any re-registration.

~/.sigil/bin/ holds stable shell shims that resolve the real binary at runtime.

Every integration points at a shim, never at a versioned package path. That’s what keeps memory working across nvm/fnm switches, reinstalls, and updates — the thing agents were told about doesn’t move.

When memory mysteriously stops working after a Node upgrade, this is what broke, and this is the fix:

Terminal window
sigil connect

It re-pins the shims and re-syncs every client’s generated config. It touches no database, no provider, and no API key.

Two drivers behind one interface.

ModeWhat it is
embeddedPGlite — Postgres compiled to WASM, running in-process against ~/.sigil/db. Zero install. Single machine only.
local / docker / urlReal Postgres 13+ with pgvector

Migrations are ordered .cjs files run with Knex, applied on sigil migrate and automatically during sigil update. Pooled connection URLs are detected and migrations routed to the direct endpoint, because pooled endpoints can’t run DDL reliably.

Embedding dimension is a build-time constant (1024) rather than configuration, so the vector(N) columns and every provider are pinned together and can never drift into a dimension mismatch.

~/.sigil/config.json is the single source of truth. Not .env — an earlier .env design was replaced because stale files broke upgrades.

The properties that matter:

  • Sparse on disk. Only explicitly-set values are written. Defaults live in code and merge at read time, so defaults track the code instead of freezing into a file.
  • Versioned. schemaVersion plus ordered migrations run on load; unknown keys are dropped.
  • Atomic writes. Temp file plus rename, chmod 600.
  • Live. The in-memory cache updates on every patch, so the daemon sees new values without a restart — except for the settings explicitly marked as needing one.

The only environment variables left are things that physically cannot live in the file: HOME (which locates the file), per-process identity for IPC, the PGlite path redirect for tests, the release-lane selector, and OS/debug flags.

Ingestion runs as durable jobs rather than inline. Entity extraction and relation discovery are follow-up jobs by default, so a write returns as soon as chunks and facts are searchable.

ingest.workerConcurrency (default 2) sets the pool. Queue depth is visible on the dashboard’s Engine screen.

An opt-in subsystem that keeps warm claude CLI workers in tmux, amortizing agentic cold-start across many ingest calls.

Off by default — when disabled, the provider transparently falls back to the one-shot path, so nothing depends on it. Only meaningful on a host with both tmux and the claude CLI. See Configuration.

The invariant: a broken memory layer must never block a working prompt.

  • Every hook wraps in a top-level try/catch that fails silently. Errors append to ~/.sigil/.hook-errors.log.
  • After five unacknowledged errors in 24 hours, sigil doctor exits non-zero so CI can catch it. Acknowledge with sigil doctor --ack.
  • A circuit breaker stops repeated daemon spawn attempts rather than retrying forever.
  • Concurrent claude processes are capped by a semaphore (llm.maxClaudeProcs, default 4). This exists because an unbounded ingest fan-out once produced 1600+ live sessions.
  • Network provider calls have a 60s timeout, so a hung socket can’t wedge the daemon or a hook.

macOS, Linux, and WSL. Native Windows is unsupported and refuses to install rather than half-working — the launcher shims and Claude Code hooks are POSIX shell scripts, and the daemon’s path model assumes a POSIX environment.