Architecture
Session management
How OperationKit spawns, isolates, supervises, and safely terminates the Claude Code processes that execute objectives — and why each design decision exists.
Execution model
Every objective runs as exactly one Claude Code session: a claude --print process invoked with a turn budget, a fully assembled context document, and a goal. The session reads, plans, writes code, calls APIs, and either completes or reports a blocker. It does not stream interactively; --print runs headlessly and exits when Claude finishes its response.
Sessions are created by session-manager.ts, the backend service that sits between the board database and the host's process table. When an objective moves to working status the manager:
- Selects a free
ccuser-*account (see Account rotation). - Creates the per-objective worktree at
/tmp/cc-worktree-{id}/. - Assembles the context document from the four-tier stack, KB hits, and objective memory.
- Opens a tmux window under the selected account's tmux server and runs
claude --print <context>inside it.
Why tmux?
The choice of tmux as the session container is deliberate and operational. A plain child_process.spawn call inside Node.js ties the Claude process's lifetime to the Node process. When the backend restarts — for a deploy, a crash, or a server reboot — any in-flight agent session is killed. At scale, with 25 concurrent sessions, this produces cascading data loss every time you ship a backend fix.
A tmux session is independent of the process that created it. The Node backend can exit, restart, and reconnect to the tmux server without disturbing running Claude processes. The session-manager reattaches by looking up the tmux window name — which it sets to the objective ID — and polling for completion. Sessions survive backend restarts entirely.
Yang et al. (2024) note in SWE-agent that the agent–computer interface — the mechanism through which an agent interacts with its execution environment — shapes agent behavior as much as the underlying model. A fragile interface (processes killed on restart) forces defensive patterns that consume context budget. A durable interface (tmux) lets the agent focus on the task.
The tradeoff is operational complexity: tmux windows accumulate, completed sessions must be explicitly reaped, and debugging requires attaching to a named window rather than reading a log file. OperationKit accepts this complexity because the alternative — restarting in-flight sessions — is worse at scale.
Per-objective worktree isolation
Before spawning a session, session-manager.ts creates a git worktree for the objective at /tmp/cc-worktree-{objective_id}/. The session works exclusively inside this directory. It never touches the live checkout at /home/mike/projects/<repo>.
The worktree is a full working tree with its own index, checked out to a feature branch named after the objective. Git operations (commit, push) run inside the worktree and are scoped to that branch. The live checkout's branch and index are untouched.
The path uses the objective ID rather than a branch name or repo name to guarantee uniqueness. Two sessions running against different repos for the same objective would still get different paths; two sessions running against the same repo for different objectives never share a directory.
The 2026-04-25 production crash
Worktree isolation became non-optional after an incident on 2026-04-25. Prior to that date, sessions operated directly inside the live checkout. A session that was debugging a routing bug in operationkit-site made a git reset --hard call while a concurrent session was mid-commit to the same directory. The reset won. The concurrent session's staged changes were discarded, and the files reverted to the commit before the other session's work.
More seriously: the Node backend process runs from the same live checkout. The reset moved the backend's source files mid-execution, causing a require-cache mismatch that corrupted three in-flight database writes. Recovery required a manual restore from a 6-hour-old snapshot.
The post-incident fix was two-layered: worktrees for path isolation, and the worktree guard hook for write-side access control. Together they make it physically impossible for a session to affect a directory it was not assigned.
The worktree directory is under /tmp and does not survive a full server reboot. If the host reboots while a session is in flight, the session's in-progress file changes are lost. The session's committed work (already pushed to the remote) is safe. OperationKit does not currently checkpoint uncommitted changes; this is a known limitation.
Supervisor model
The entrypoint for the OperationKit backend is entrypoint.sh: a shell script that starts the Node process and loops on it forever.
#!/bin/bash
# entrypoint.sh — the entire supervisor
while true; do
node dist/index.js
echo "process exited with $? — restarting in 2s"
sleep 2
done
There is no pm2, no systemd unit, no foreman. The supervisor is a bare while true loop that restarts the Node process if it exits for any reason — crash, OOM kill, or explicit shutdown. The 2-second sleep prevents a tight restart loop from consuming CPU during a persistent crash.
Design rationale
The decision to use a bare shell loop rather than a process manager is pragmatic rather than principled. OperationKit runs on a single Docker container that is itself managed by the host's container runtime. A crash that kills Node is almost certainly a bug in the application, not a workload scheduling problem. pm2's cluster mode and systemd's socket activation add complexity that has no payoff in a single-process, single-container deployment.
What breaks
Honesty requires documenting the known failure modes of this approach:
| Failure | Behavior | Mitigation |
|---|---|---|
| Node process crash | All pending HTTP requests are dropped. Clients see a connection reset. | 2-second restart. In-flight sessions survive in tmux and are reattached on reconnect. |
| OOM kill | Same as crash. If the OOM condition persists, the loop crashes on restart too. | Container memory limit alerts via the host's monitoring. No automatic remediation. |
| Persistent crash loop | The 2-second sleep throttles CPU, but the process restarts indefinitely. No circuit breaker. | Logs emit the exit code on each restart. Operator must intervene. |
| Graceful drain on deploy | No graceful shutdown hook. A deploy kills the process mid-request. | Accepted: deploys are rare and the restart is fast. In-flight sessions are not affected (tmux). |
A production system serving external customers would want a proper process manager, health checks, and a graceful drain path. OperationKit trades that reliability ceiling for operational simplicity, which is reasonable for an internal tool running on a known-good VPS with low traffic.
Account rotation
Each Claude Code session runs under a distinct system account: ccuser-0 through ccuser-4. The session-manager maintains a pool of these accounts and assigns one to each new session, rotating to the next free account when all slots under the current account are occupied.
The reason is Anthropic's per-account API rate limits. Claude Code sessions make API calls against the account's Claude subscription. A single account with 25 concurrent sessions would saturate its rate limit budget and begin returning 429s, stalling all sessions simultaneously. Five accounts with 5 sessions each distribute the load: a rate-limit event on one account stalls only its 5 sessions while the other 20 continue.
What rotation buys
Account rotation is not a reliability mechanism — it doesn't prevent rate limiting, it distributes its blast radius. The practical effect is that the board can sustain 25 concurrent sessions without systematically stalling, as long as no single account's 5 sessions all make simultaneous heavy API calls. In practice this is rare: sessions interleave tool calls (filesystem reads, git operations, API calls) with model inference, and the inference spikes are spread across different phases of different objectives.
Each account has its own tmux server, home directory, and shell environment. This provides incidental isolation: a session under ccuser-2 cannot accidentally read ccuser-0's shell history or environment variables. The worktree guard provides stronger isolation for filesystem writes (see below); account separation is defense-in-depth for the shell layer.
Liu et al. (2023) in AgentBench document that real-world agent deployments face infrastructure constraints — rate limits, resource contention, host memory — that are entirely orthogonal to the agent's reasoning capability. Account rotation is OperationKit's answer to the rate-limit constraint: it reframes it as a scheduling problem rather than a capacity problem.
The worktree guard as write-side access control
The worktree guard is a PreToolUse hook registered in every session's Claude Code configuration. It intercepts every file-write tool call — Write, Edit, NotebookEdit, and shell commands that write to disk — before execution and validates that the target path falls inside the session's assigned worktree.
# worktree-guard.sh (simplified)
ASSIGNED_WORKTREE="/tmp/cc-worktree-${OBJECTIVE_ID}"
TARGET_PATH="$1"
# Resolve to canonical absolute path
RESOLVED=$(realpath -m "$TARGET_PATH" 2>/dev/null || echo "$TARGET_PATH")
case "$RESOLVED" in
"${ASSIGNED_WORKTREE}/"*)
exit 0 # allowed: inside assigned worktree
;;
/tmp/*)
exit 0 # allowed: scratch space
;;
*)
echo "BLOCKED: write to '$TARGET_PATH' outside worktree" >&2
exit 1 # denied: Claude Code sees a tool call rejection
;;
esac
A rejection causes Claude Code to treat the tool call as failed and include the error message in the model's next turn. The model sees "BLOCKED: write to … outside worktree" and can adjust — typically by writing to a path inside its worktree or explicitly asking for permission. It cannot bypass the hook; the hook runs at the harness layer, below the model.
Why this is the only guard that matters
The worktree directory structure provides path isolation, but path isolation is not access control. A session that knows the path of another session's worktree can still write to it if the filesystem allows it. Under a naive setup, all ccuser-* accounts have write access to /tmp, which means any session can write to any other session's worktree.
The worktree guard closes this gap at the tool call layer, not the filesystem layer. This is intentional: Claude Code is the only thing making filesystem writes in a session. Blocking at the Claude Code tool layer is sufficient because there is no other write path. A session that tried to bypass the guard by running raw shell commands through the Bash tool would still hit the guard — the hook intercepts Bash tool calls too, scanning the command string for write operations against out-of-scope paths.
The guard deliberately does not use Linux filesystem permissions (chown, chmod, ACL). Filesystem ACLs are coarser, harder to update per-session, and create maintenance overhead when accounts are rotated. The tool-layer approach is scoped to exactly the capabilities Claude Code exposes, which is the correct scope: we are controlling what the agent can do, not what the account can do.
The 2026-04-25 production crash (documented in Worktree isolation) would not have occurred if the worktree guard had been in place. The session that called git reset --hard was operating inside the live checkout — a path that the guard would have blocked. The guard was retroactively identified as the missing safeguard and was added as part of the post-incident remediation.
Yang et al. (2023) in InterCode observe that agent systems operating in interactive coding environments need execution feedback — the environment must tell the agent what happened, including what was blocked. The worktree guard implements this: the rejection message is surfaced directly into the model's context, giving it accurate information about why the write failed and what it should do instead. A silent block (filesystem permission denied with no message) would cause the model to retry blindly.
References
The engineering decisions above draw on published research in agent systems, coding agents, and interactive execution environments.
-
SWE-agent: Agent-Computer Interfaces Enable Automated Software Engineering
Why it matters here: Establishes the agent–computer interface (ACI) as a first-class design concern: the mechanism through which an agent interacts with its environment shapes behavior as much as the underlying model. OperationKit's tmux container and worktree directory are its ACI — designed to be durable (sessions survive backend restarts) and bounded (writes cannot escape the assigned worktree).
arxiv.org/abs/2405.15793 ↗ -
AgentBench: Evaluating LLMs as Agents
Why it matters here: Documents the gap between LLM benchmark performance and real-world agent deployment, including infrastructure constraints like rate limits and resource contention that are orthogonal to model capability. Motivates OperationKit's account rotation design: distributing sessions across accounts is a scheduling solution to an infrastructure constraint, not a model-quality concern.
arxiv.org/abs/2308.03688 ↗ -
InterCode: Standardizing and Benchmarking Interactive Coding with Execution Feedback
Why it matters here: Demonstrates that agents operating in interactive coding environments require execution feedback — the environment must communicate what happened, including what failed and why. Directly informs the worktree guard's design: rejection messages are surfaced into the model's context so it can adapt, rather than failing silently and causing blind retries.
arxiv.org/abs/2306.14898 ↗ -
OSWorld: Benchmarking Multimodal Agents for Open-Ended Tasks in Real Computer Environments
Why it matters here: Benchmarks agents operating in real computer environments and measures the gap between sandbox success and real-environment success — isolation and sandboxing that work in benchmarks must also hold in production. Informs OperationKit's decision to enforce worktree isolation at the tool layer (not just at setup time), because the real environment contains live data and concurrent processes that benchmarks typically abstract away.
arxiv.org/abs/2404.07972 ↗ -
Voyager: An Open-Ended Embodied Agent with Large Language Models
Why it matters here: Introduces the concept of a persistent skill library that agents accumulate across sessions — analogous to OperationKit's skills layer — and demonstrates that long-running agents need external memory and execution continuity across restarts. The tmux-based session model directly addresses the continuity requirement: skills persist on disk, sessions persist in tmux, and neither is lost when the orchestrating process restarts.
arxiv.org/abs/2305.16291 ↗