Configuration
Tandem’s configuration surface stays small on purpose. Five things to know:
agent definitions (agents.json), the mc_settings toggles surfaced under
/admin, the environment variables the backend reads, the three ports services
bind to, and the targeted restart commands. Everything else is convention.
agents.json
The agent roster lives in project-workspace/Tandem/app/agents.json as a
single JSON document with a top-level "agents": [...] array. Each entry is one
named agent. The schema below is enforced by the backend on read; missing
required fields cause that agent card to be skipped (with a warning in the API
log).
Schema (per-agent fields)
| Field | Type | Purpose |
|---|---|---|
id | string | Stable slug. Used by mc.py claim-task <id> <agent-id>, in URLs (/agents/<id>), and as the worktree branch suffix. |
name | string | Display name on the Agents page. |
description | string | One-line summary. Renders below the card name. |
project | string | Owning project. Use "General" for cross-project agents. |
default_tool | string | Default LLM (e.g. opus, sonnet, haiku, codex, ollama/qwen3.6:35b). |
system_prompt | string | Full role prompt. Multiline strings allowed. Required. |
tools | string[] | Whitelist of tools this agent may invoke (e.g. Read, Grep, Bash, mc.py). Enforced by the per-agent ACL. |
constraints | string[] | Human-readable rules surfaced in the agent card and prepended to dispatch prompts. |
skills | string[] | Skill names loaded for this agent (matches .claude/skills/ entries). |
autonomous_config | object | Per-agent ACL. Keys: worktree_isolation (bool), allowed_dirs (path[]), network_access (local/none/full), max_sessions (int). |
Example: a minimal writing agent
{ "id": "doc-updater", "name": "Doc Updater", "description": "Keeps README, CHANGELOG, and wiki pages aligned with code.", "project": "Tandem", "default_tool": "sonnet", "system_prompt": "You are a documentation writer. Read the diff, identify which docs are affected, and update them in the same PR. Never rewrite history — only patch.\n\nTASK TRACKING: Use `python3 mc.py update-task <id> --status active` when starting, and append to `## Activity Log` when done.", "tools": ["Read", "Grep", "Glob", "Edit", "Write", "Bash"], "constraints": [ "Only edit files under docs/, README.md, CHANGELOG.md, or wiki/", "Never delete a doc — archive it under project-workspace/archive/", "Cite the source commit hash in every wiki edit" ], "skills": ["documentation-and-adrs", "obsidian-markdown"], "autonomous_config": { "worktree_isolation": true, "allowed_dirs": [ "project-workspace/Tandem/app", "wiki", "docs" ], "network_access": "local", "max_sessions": 2 }}mc_settings
Live toggles that change runtime behavior without a restart. Stored in the
mc_settings table in tandem.db; surfaced and edited under /admin. Every
toggle is also exposed via GET /api/mc-settings/<key> +
POST /api/mc-settings/<key>.
| Key | Values | What it controls |
|---|---|---|
agent_restart_mode | gated · free | Whether agents may restart services without an explicit user prompt. gated (default) routes via the MiniFixService popover or asks y/n; free lets the agent pick the targeted restart but never run a full mc.py fix while a long-running job is in flight. |
sandbox_enabled | off · on | Admin-gated bubblewrap sandbox for agent-generated code (off by default). When on, code runs inside bwrap via systemd-run; verify the host with python3 -m sandbox.prereq. |
auto_snapshot | on · off | Per-agent post-action snapshot hook (.claude/hooks/post-agent-snapshot.sh). When on, dirty worktrees auto-commit on Agent tool return with prefix auto-snapshot: as a safety net. |
approvals_default | deny · prompt | Default disposition for agent actions outside their allowed_dirs. deny short-circuits silently; prompt opens an approval card under /approvals. |
cost_panel_provider | anthropic · codex · both | Which provider’s JSONL transcripts the /costs page reads. anthropic pulls claude --print output; codex reads the Codex CLI cache. |
spark_worker_enabled | off · on | Whether spark-worker.py picks queued briefs. When off, briefs queue up but are never dispatched — useful for staging template changes. |
scheduled_jobs_enabled | off · on | Master switch for spark-scheduler.py cron-style triggers. Off pauses every scheduled brief at the clock-match step without losing state. |
Environment variables
Read once at process start. Set in ~/.tandem/env, the systemd unit’s
Environment= stanza, or export inline before tandem.sh.
Core
| Variable | Default | Purpose |
|---|---|---|
TANDEM_API_BASE | http://127.0.0.1:4200 | API base URL. Every helper (mc.py, spark-worker, spark-scheduler, terminal-server) reads it. |
MC_FRONTEND_PORT | 3000 | Override the Next.js port. Always bind 127.0.0.1. |
MC_API_PORT | 4200 | Override the API port. |
MC_TERMINAL_PORT | 4201 | Override the WebSocket terminal port. |
TANDEM_VAULT | ~/Desktop/claude-brain | Absolute path to the Obsidian vault. Used by tasklib.py, the wiki ingest pipeline, daily-notes append. |
TANDEM_DB | <tandem-dir>/tandem.db | SQLite path. Override for testing. |
Spark (queue + scheduler)
| Variable | Default | Purpose |
|---|---|---|
SPARK_VAULT | $TANDEM_VAULT | Vault root passed into bwrap-spark.sh. |
SPARK_TANDEM_DIR | dir of spark-worker.py | Where bwrap-spark.sh + spark-settings.json live. |
SPARK_AGENTS_JSON | <tandem>/agents.json | Used to resolve brief.template_id → spark-* system_prompt. |
SPARK_PHASE_TIMEOUT | 900 | Wall-clock cap (seconds) per author phase. Accommodates cold qwen3.6:35b load. |
SPARK_OLLAMA_FALLBACK | http://127.0.0.1:11434 | Used if /api/settings/ollama-primary-url is missing. |
SPARK_NETWORK_ALLOW | 1 | Forwarded to bwrap-spark.sh; set 0 to enforce --unshare-net. |
SPARK_LOG_FILE | $TANDEM_VAULT/tandem-outputs/spark-worker.log | Append-only log path. |
SPARK_SCHEDULER_STATE | <tandem>/data/spark-scheduler-state.json | Last-fire-date persistence so restart-same-day doesn’t double-fire. |
Offline / local-only
| Variable | Default | Purpose |
|---|---|---|
CLAUDE_OFFLINE | unset | When 1, agents must not pass a model param on Agent tool calls and must skip Anthropic-only built-in subagents. Local model required. |
TANDEM_NPM_TIMEOUT_SECS | 900 | Max seconds for the npm install step on macOS install. Bump for slow links. |
TANDEM_BROWSER | (prompt) | Auto-pick browser for macOS install — safari, brave, chrome, or skip. |
Port config
Three services. All bind 127.0.0.1 only — the install hardening refuses
0.0.0.0.
| Port | Service | Process | Override |
|---|---|---|---|
3000 | Frontend (Next.js + Tailwind + shadcn/ui) | npm run dev via tandem.sh | MC_FRONTEND_PORT |
4200 | API (HTTP, REST) | python3 server.py | MC_API_PORT |
4201 | Terminal (WebSocket, node-pty, tmux-persistent) | node terminal-server.js | MC_TERMINAL_PORT |
Verify they are bound
ss -tln | grep -E ':(3000|4200|4201)\b'
# Expected output — three rows, all 127.0.0.1:# LISTEN 0 ... 127.0.0.1:3000 0.0.0.0:*# LISTEN 0 ... 127.0.0.1:4200 0.0.0.0:*# LISTEN 0 ... 127.0.0.1:4201 0.0.0.0:*If any row shows 0.0.0.0:PORT instead of 127.0.0.1:PORT, that service was
started without the right bind flag — stop it and re-launch via tandem.sh
(which enforces the bind).
Service restart
Use the most targeted restart command possible. Never run a full mc.py fix
when only one service needs bouncing — it nukes the frontend Next.js cache and
costs ≈60 s every time.
| Command | Restarts | Time | When to use |
|---|---|---|---|
mc.py fix | Backend + frontend + caches | ~60 s | Corrupted state, first boot, total reset. |
mc.py fix backend | Backend only (server.py) | ~3 s | After editing server.py, tasklib.py, config.py or any Python backend file. |
mc.py fix frontend | Frontend only (Next.js dev server) | ~10–60 s | After editing frontend/ files that need a dev-server restart. |
mc.py fix terminal | Terminal server only | ~2 s | After editing terminal-server.js, or when WebSocket connections drop. |
Aliases
mc.py repair— same asmc.py fix.mc.py debug— same asmc.py fix, with extra stderr noise.- Both aliases accept the same subcommands:
backend,frontend,terminal.
Rules for agents
- If you edited
server.py→mc.py fix backend(NOTmc.py fix). - If you edited frontend components →
mc.py fix frontend. - If you edited
terminal-server.js→mc.py fix terminal. - Full
mc.py fixonly when multiple services are broken or caches are corrupted.
From the Tandem UI
If you’re in the MiniFixService popover (sidebar header), every restart above is a button:
- Terminal →
mc.py fix terminal - Backend →
mc.py fix backend - Frontend →
mc.py fix(frontend implies the full path) - Full →
mc.py fix
Agents emit a =======RESTART <AREA> REQUEST======= banner and fire
POST /api/ui/highlight {target, button} to open the popover with the button
pulsing. No permission prompt, no CLI paste.