A Go MCP server that lets orchestrators (Claude Code, Hermes, cabinet) create and manage isolated Claude Code sessions. Each "workspace" is a git worktree in a dedicated directory, opened inside a named tmux session with a running Claude Code interactive shell.
| Tool | Minimum version |
|---|---|
| Go | 1.22 |
| tmux | 3.2 |
| git | 2.35 |
| claude (Claude Code CLI) | any |
make build # produces ./hangar
# or
go build -o hangar .Configuration can be supplied via a JSON file and/or environment variables. Environment variables always take priority.
| Field | Env var | Default | Description |
|---|---|---|---|
repoPath |
HARNESS_REPO_PATH |
(required) | Absolute path to the git repository being managed |
worktreeRoot |
HARNESS_WORKTREE_ROOT |
<repoPath>/../worktrees |
Directory where worktrees are created |
storePath |
HARNESS_STORE_PATH |
~/.config/hangar/workspaces.json |
Path to the JSON workspace registry |
claudeCmd |
HARNESS_CLAUDE_CMD |
claude |
Command to launch Claude Code |
idleThresholdMs |
HARNESS_IDLE_THRESHOLD_MS |
5000 |
Milliseconds of pane inactivity before a session is "idle" |
sessionPrefix |
HARNESS_SESSION_PREFIX |
harness- |
Prefix for tmux session names |
maxWorkspaces |
HARNESS_MAX_WORKSPACES |
10 |
Hard cap on concurrent active workspaces (1–50) |
Example config file (harness-config.json):
{
"repoPath": "/home/alice/myproject",
"maxWorkspaces": 5,
"idleThresholdMs": 8000
}Add the server to your Claude Code MCP config (typically ~/.claude/mcp.json or a per-project .mcp.json):
{
"mcpServers": {
"hangar": {
"command": "/usr/local/bin/hangar",
"args": ["--config", "/home/alice/.config/hangar/config.json"],
"env": {
"HARNESS_REPO_PATH": "/home/alice/myproject"
}
}
}
}The orchestrator Claude Code instance will then have access to all workspace tools.
Generic stdio MCP entry (adjust command and env as needed):
{
"servers": [
{
"name": "hangar",
"transport": "stdio",
"command": "/usr/local/bin/hangar",
"args": ["--config", "/home/alice/.config/hangar/config.json"],
"env": {
"HARNESS_REPO_PATH": "/home/alice/myproject"
}
}
]
}List all workspaces. By default excludes archived and orphaned ones.
Inputs: include_archived (bool, optional, default false)
Output: JSON array: [{id, name, status, branch, tmuxSession, createdAt, worktreePath}]
Create a new workspace: git worktree + tmux session + Claude Code instance.
Inputs:
name(string, required) — lowercase alphanumeric and hyphens, 1–40 charsbranch(string, optional) — git branch to create; defaults tonamemeta(object, optional) — freeform string key-value metadata
Output: Full workspace object as JSON.
Gracefully shut down a workspace. Exits Claude Code, removes the worktree, retains the git branch.
Inputs: id (string, required)
Output: Updated workspace object with status: "archived".
Permanently delete a workspace and its git branch. Irreversible.
Inputs:
id(string, required)confirm(bool, required) — must betrue
Output: {"deleted": true, "id": "<id>"}
Send text to the Claude Code session in a workspace.
Inputs:
id(string, required)text(string, required) — must not contain ASCII control characters (except\nand\t)press_enter(bool, optional, default true)
Output: {"sent": true}
Guards: Workspace must be active. Rate limited to 1 call per 200 ms per workspace.
Capture recent terminal output from a workspace's tmux pane.
Inputs:
id(string, required)lines(int, optional, default 200, max 2000)
Output: {"content": "...", "captured_at": "<ISO 8601>"}
Check whether a workspace is busy or idle.
Inputs:
id(string, required)threshold_ms(int, optional) — override the configured default
Output: {"Idle": bool, "LastChangedAt": "...", "ElapsedMs": N, "ThresholdMs": N}
Block until the workspace becomes idle or the timeout elapses. Polls pane output internally so the caller makes a single tool call instead of polling repeatedly.
Inputs:
id(string, required)timeout_ms(int, optional, default600000= 10 min) — maximum time to waitthreshold_ms(int, optional) — idle-stability threshold overridepoll_interval_ms(int, optional, default500) — how often to sample the pane
Output (success — workspace became idle):
{
"idle": true,
"timed_out": false,
"last_changed_at": "2026-06-05T11:30:00Z",
"elapsed_ms": 5200,
"threshold_ms": 5000
}Output (timeout elapsed before idle):
{
"idle": false,
"timed_out": true,
"last_changed_at": "2026-06-05T11:29:55Z",
"elapsed_ms": 600000,
"threshold_ms": 5000
}Hard errors (workspace not found, not active, or pane capture failure) are returned as MCP tool errors, not JSON.
Return the shell command to attach to a workspace's tmux session.
Inputs: id (string, required)
Output: {"command": "tmux attach-session -t harness-<name>"}
The idle detector does not parse Claude Code's internal state. Instead:
- Capture the last 200 lines of the tmux pane via
tmux capture-pane. - SHA-256 hash the output.
- If the hash changed since the last check → busy (hash + timestamp stored in the workspace registry).
- If the hash is unchanged, compute elapsed ms since last change:
- elapsed ≥
threshold_ms→ idle - elapsed <
threshold_ms→ busy
- elapsed ≥
Tuning: Increase idleThresholdMs if your Claude Code sessions take a long time to produce output between steps. Decrease it for faster polling in interactive use.
Any workspace can be inspected or interacted with by a human at any time:
# Get the attach command via MCP:
workspace_attach_hint id=<workspace-id>
# Or directly (if you know the name):
tmux attach-session -t harness-<name>The orchestrator and human can both interact with the session simultaneously.
Orchestrator Claude Code (has hangar MCP registered)
│
│ workspace_create / workspace_send / workspace_idle / workspace_read / workspace_archive
▼
hangar binary
│
├── git worktrees (one per workspace)
└── tmux sessions (one per workspace, named harness-<name>)
└── Worker Claude Code instances (one per session)
Human ──► tmux attach-session -t harness-<name> (at any time)
Typical orchestrator flow:
- Call
workspace_create {name: "feat-foo"}→ workspace created, Claude Code launches. - Call
workspace_send {id: ..., text: "Implement feature X"}→ prompt sent. - Call
workspace_wait_idle {id: ..., timeout_ms: 600000}→ blocks until Claude Code finishes (or times out). - Call
workspace_read {id: ..., lines: 500}to retrieve output. - Optionally attach (
tmux attach-session -t harness-feat-foo) to verify. - Call
workspace_archive {id: ...}when done.
On startup, hangar reconciles the workspace registry against live tmux sessions:
- Active workspace, session missing → status set to
orphaned, logged to stderr. The branch and registry entry are preserved. - tmux session with harness prefix, not in registry → warning logged (not auto-deleted; may be manually created).
- One git repository per server instance. Use separate binaries for multiple repos.
- No authentication on the stdio transport. Secure your process environment.
- Idle detection is heuristic (hash-based); a session that produces the same output repeatedly may appear idle prematurely.
"Claude Code didn't launch in tmux"
Check HARNESS_CLAUDE_CMD points to a valid binary. The session is still created — attach to it manually and launch claude to inspect.
"worktree already exists"
A previous run left a stale worktree. Run git worktree prune in the repo, or use workspace_delete to clean up via the MCP interface.
"store is out of sync"
Delete ~/.config/hangar/workspaces.json and restart. Existing tmux sessions will show as untracked warnings at next startup.
"session shows busy indefinitely"
Increase idleThresholdMs. Or attach to the session manually to check whether Claude Code is actually stuck.