The secure, self-hosted way to run a fleet of Claude Code agents — each behind a kernel-enforced egress firewall or a secret-zero credential broker.
docker-compose for Claude Code — on infrastructure you own.
myclementine.ai · Quickstart · Download · Discord
clem runs a team of Claude Code agents 24/7 on any Linux host. Each agent is a separate OS user in a tmux session under systemd. Agents coordinate over Discord, Slack, or GitHub Issues, pick up tasks, write code, and open PRs. A watchdog restarts anything that crashes. You write one clem.yaml; clem provisions the OS users and keeps them running.
What sets it apart: secrets and egress are contained at the OS layer, not by the agent's cooperation. Each agent takes one disposition — a per-UID kernel firewall that forces all egress through an auditing proxy (a non-root agent can't disable a firewall it doesn't own), or a secret-zero broker that hands it only placeholders while a separate user injects the real credential on egress. Enforced by the kernel and a separate user, not by the agent. See the security model.
| Per-agent OS identity | Each agent is its own Linux user - own home dir, own git identity, own GitHub PRs, own Discord/Slack bot. Crash boundaries are real. |
| Kernel egress containment | Per-agent nftables UID firewall forces all traffic through a loopback proxy; a non-root agent can't disable a firewall it doesn't own. No in-process escape hatch. Opt-in egress: block. |
| Secret-zero brokering | Brokered agents hold placeholders + a scoped inject-only token; real credentials live in a vault owned by a separate user and are injected on egress. cat ~/.env yields nothing usable for the brokered keys. |
| Multi-backend coordination | Discord, Slack, or GitHub Issues via swappable coordination.backend: in clem.yaml. One config knob. |
| Multi-runtime | runtime: claude-code | opencode. Mix Anthropic cloud, Bedrock, Vertex, Ollama, OpenAI-compat - one surface. |
| Encrypted secrets | Per-agent .env materialised from age/sops vaults at provision time. Never leave the host after. |
| Self-healing | systemd + tmux per agent. Watchdog timer restarts dead or stalled sessions. Alerts fire only after repeated failures. |
| Bring your own model | Default Claude; one flag away from Ollama Cloud / Bedrock / Vertex / local models. Tested end-to-end on local Gemma 4 (E4B QAT via Ollama). |
| Live ops | clem status shows health per agent. Optional ttyd web terminal per agent - attach in your browser. |
| Works locally | Laptop, home server, Raspberry Pi, small VPS. No Kubernetes. No cloud services required. |
- How it works
- Security model
- Requirements
- Install
- Quickstart
- Coordination backends
- Discord setup
- GitHub coordination
- GitHub credentials
- CLI reference
clem.yamlreference- Secrets
- Deploy to a VPS
- Troubleshooting
- License
┌──────────────────────────────────────────────────────┐
│ Linux host (your laptop · home server · VPS · …) │
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ OS user: │ │ OS user: │ systemd + │
│ │ myteam-lead │ │ myteam-worker│ tmux per user │
│ │ claude loop │ │ claude loop │ │
│ └──────┬───────┘ └──────┬───────┘ │
│ └──── MCP (stdio) ─┘ │
│ │ │
│ ┌──────────────────┴──────────────────┐ │
│ │ watchdog timer (every 5 min) │ │
│ │ restarts dead agents → #alerts │ │
│ └─────────────────────────────────────┘ │
└───────────────────┬──────────────────────────────────┘
│ coordination backend
┌─────────▼──────────────────────────┐
│ Discord · Slack · GitHub Issues │
│ #tasks / threads · labels · gh │
└────────────────────────────────────┘
Each agent runs a loop: launch claude (or opencode), inject a prompt, wait for the session to finish (up to 2h hard cap), sleep the configured iteration duration, repeat. Secrets live encrypted in secrets.sops.yaml (age/sops); clem provision decrypts them into per-agent .env files on the host.
An autonomous agent is an untrusted workload: prompt injection, a poisoned dependency, or a model mistake can turn it into an exfiltration engine. clem's stance is contain it at the OS layer, not by asking the agent nicely. Every credential an agent would otherwise hold gets exactly one of four dispositions:
| Disposition | Mechanism | The real secret lives… | Threat closed |
|---|---|---|---|
| broker | a credential proxy (separate UID) injects the real value into the agent's own outbound HTTPS | inside the broker | API-key / bearer exfiltration — the agent only holds a placeholder |
| sidecar | a secret-holding MCP server runs as a separate user; the agent calls it over loopback and gets a result, never the key | inside the sidecar | non-HTTP creds (gateway tokens, internal DBs) and scoped/read-only access |
| remove | drop the credential/MCP entirely | nowhere | unused attack surface |
| egress firewall | per-agent nftables UID rule forces all traffic through a loopback proxy; everything else is rejected by the kernel | n/a | data exfiltration to unapproved hosts |
Why this is stronger than in-process or single-container sandboxes: the boundary is a per-OS-UID kernel firewall a non-root agent cannot disable, plus a credential broker running as a different user the agent cannot read — neither depends on the agent's cooperation, and there is no in-process escape hatch. A compromised agent holds no usable secrets and can reach no unapproved network.
Honest about the parts that are borrowed: the egress proxy and credential broker are battle-tested OSS primitives (pipelock, Infisical agent-vault). clem's contribution is the OS-level composition — per-agent UID identity + kernel firewall + secret supply, wired so the agent literally cannot route around either.
→ Full threat model, guarantees, and known limitations: docs/threat-model.md. → Worked reference config: samples/secure-fleet/.
Both layers are opt-in and default-off; existing fleets are unaffected until you enable egress: / vault.backend: agent-vault.
Host - any Linux box with systemd (Ubuntu 24.04 recommended). Can be your laptop, a home server, a Pi, or a cloud VPS. Must have tmux, git, python3, age, sops, yq, and curl. Chat backends also need their MCP server on $PATH (mcp-discord for Discord, slack-mcp-server for Slack). The GitHub backend uses the gh CLI instead of a coordination MCP. clem provision installs the runtime CLI (Claude Code or opencode) per agent.
Local machine - where you run clem commands (may be the same box as the host):
go1.22+ (to buildclem)age,sops,yq- to edit secrets locallygh- GitHub CLI
Accounts:
- A coordination backend (pick one):
- Discord - a private server + one bot token per agent, or
- Slack - a workspace + one Slack app per agent (bot user token
xoxb-…), or - GitHub Issues - a task-board repo with
clem:*labels +GH_TOKENper agent (same token used for PRs)
Download the latest release (Linux):
# x86-64
curl -fsSL https://github.com/jahwag/clem/releases/latest/download/clem_linux_amd64 -o /usr/local/bin/clem && sudo chmod +x /usr/local/bin/clem
# arm64: swap clem_linux_amd64 -> clem_linux_arm64
clem --versionOr build from source:
git clone https://github.com/jahwag/clem.git
cd clem
go build -ldflags "-X github.com/jahwag/clem/cmd.Version=$(git describe --tags --always)" -o clem .
sudo install -m 0755 clem /usr/local/bin/clem
clem --versionTo upgrade:
sudo clem updateFull local setup on one Linux box. If you want to provision on a separate remote host, see Deploy to a VPS.
Try clem without touching your host: sandboxed samples under samples/ -
ollama-nemotron-4b- Discord + local NVIDIA Nemotron 3 Nano 4B (~2.8 GB)slack-nemotron-4b- Slack + same local modelgithub-tasks- GitHub Issues coordination viaghCLI (no chat MCP)
# 1. new team repo (replace with your org)
gh repo create my-team --private --clone && cd my-team
# 2. scaffold config (discord is default; use --backend github for GitHub Issues)
clem init
# clem init --backend githubEdit clem.yaml:
- Set
project:(becomes OS user prefix, e.g.myteam-lead) - Pick
coordination.backend:(discord,slack, orgithub) - Discord/Slack: paste server/workspace ID and channel IDs - see Discord setup and
samples/slack-nemotron-4b/README.md. - GitHub: set
github_repoand channel mappings - see GitHub coordination. - Adjust agent
name,role,model,iteration(Go duration:30s,1m30s,2h),runtime,provider
Edit CLAUDE.shared.md - describe your project, fill in tiers T2-T4. Edit each CLAUDE.<agentkey>.md with per-agent specifics.
# 3. generate age keypair + .sops.yaml
clem vault init
# 4. store per-agent secrets (see Discord/GitHub setup below)
clem vault set github GH_TOKEN="ghp_..."
clem vault set discord-lead DISCORD_TOKEN="Bot <lead-bot-token>"
clem vault set discord-worker DISCORD_TOKEN="Bot <worker-bot-token>"
# 5. commit config (secrets.sops.yaml is encrypted - safe)
git add clem.yaml CLAUDE.*.md .sops.yaml secrets.sops.yaml
git commit -m "init team config"
git push
# 6. provision - creates OS users, installs services, writes .env
sudo clem provision
# 7. authenticate each agent with Claude (opens browser per agent)
sudo clem login
# 8. start and check
sudo clem up
clem statusclem status shows systemd state, tmux liveness, token expiry, and last log line per agent. Once SYSTEMD=active and TMUX=yes, agents are running.
Watch an agent:
clem logs lead| Backend | Task board | Claim | Alerts | Wake mechanism | MCP for coordination |
|---|---|---|---|---|---|
discord (default) |
#tasks forum threads |
Thread prefix [TODO] → [IN PROGRESS] |
#alerts channel |
mcp-discord gateway watcher |
mcp-discord |
slack |
#tasks top-level messages + threads |
Reaction emoji on top message | #alerts channel |
Agent polls on each iteration | slack-mcp-server |
github |
Issues with clem:* labels |
Self-assign via gh issue edit |
Comment on alerts issue | clem-github-watch sidecar polls Issues API |
None (gh CLI) |
GitHub coordination closes the loop between tasks and PRs: work lives in Issues on a dedicated repo, output lands in PRs with Closes #N. Chat backends stay better for real-time operator conversation; GitHub is better when your source of truth is already on GitHub.
Create a private Discord server (not a public one). Discord membership is the access control layer - agents act on instructions from anyone who can post in the channels.
Channels to create:
| Name | Type | Purpose |
|---|---|---|
#general |
Text | Status updates, operator comms |
#tasks |
Forum | Task board - agents claim threads here |
#alerts |
Text | Critical issues, watchdog alerts |
#lessons |
Forum | Post-mortems, learnings |
Enable Developer Mode (Settings → Advanced), then right-click the server icon and each channel to copy their IDs into clem.yaml.
Bot per agent - one application per agent gives each a distinct name and avatar in task threads:
- https://discord.com/developers/applications → New Application (name it after the agent)
- Bot tab → Reset Token → copy
- Enable Server Members Intent and Message Content Intent
- OAuth2 → URL Generator: scopes
bot; permissionsSend Messages,Read Message History,Attach Files,Manage Threads,Create Public Threads - Open the generated URL in a browser, add the bot to your server
- Save the token:
clem vault set discord-<agentkey> DISCORD_TOKEN="Bot <token>"
Repeat per agent.
Use GitHub Issues as the task board instead of Discord or Slack. Agents discover and claim work with the gh CLI; clem provision installs a per-agent issue watcher sidecar that polls api.github.com and wakes the tmux session when new claimable issues appear.
Prerequisites:
- A task-board repository (can be separate from the code repos agents edit).
- Labels on that repo:
clem:todo,clem:in-progress,clem:done,clem:blocked. - Two meta-issues for watchdog alerts and post-mortems; note their issue numbers.
ghCLI on the host andGH_TOKENin each agent's vault (see GitHub credentials).
Scaffold:
clem init --backend githubclem.yaml shape:
coordination:
backend: github
github_repo: "your-org/your-tasks"
channels:
tasks: "clem:todo" # label marking claimable work
alerts: "12" # issue number for watchdog / critical alerts
lessons: "34" # issue number for post-mortems
operator:
github_logins: ["your-github-login"]
agents:
lead:
vaults: [github]
# ...Task board convention:
| Concept | GitHub primitive |
|---|---|
| Task | Open issue with label clem:todo |
| Status | Labels: clem:todo → clem:in-progress → clem:done or clem:blocked |
| Claim | gh issue edit N --add-assignee @me, then re-read the issue to confirm you won the claim |
| Updates | Comment on the issue |
| Output | PR with Closes #N in the body |
| Alerts | Comment on the alerts issue (channels.alerts) |
Provisioned services (GitHub backend):
clem-<project>-<agent>.service- agent runner (unchanged)clem-github-watch-<project>-<agent>.service- polls for unassignedclem:todoissues every 60s and sendstmux send-keysto wake the agent
With egress: enabled, api.github.com is automatically added to the egress allowlist when backend: github. The watcher respects the same loopback proxy as the agent.
Full walkthrough: samples/github-tasks/README.md.
Each agent needs its own GitHub token so PRs and commits show distinct authors. Required for all backends; with backend: github the same token also drives coordination.
Fine-grained PAT (simplest, good for personal projects):
- https://github.com/settings/tokens?type=beta → Generate new token
- Select the target repositories
- Permissions:
Contents(RW),Pull requests(RW),Issues(RW),Workflows(RW) clem vault set github GH_TOKEN="ghp_..."(or a per-agent vault if you want separate tokens)
Git identity per agent - so PRs are authored by the agent's name, not root. Run after clem provision:
sudo -u myteam-lead git config --global user.name "Amara"
sudo -u myteam-lead git config --global user.email "amara@yourproject.com"
sudo -u myteam-lead git config --global credential.helper store
echo "https://amara:ghp_...@github.com" | \
sudo -u myteam-lead tee /home/myteam-lead/.git-credentialsRepeat per agent.
GitHub App (recommended for teams) - create one app per agent, exchange the private key for a short-lived installation token each iteration. See GitHub App authentication.
clem --version Print the installed version
clem update Download and install the latest release
clem init [--backend discord|github]
Scaffold clem.yaml + CLAUDE.{shared,<agent>}.md
clem vault init Generate age keypair + .sops.yaml
clem vault set <vault> KEY=value Set a secret in a vault
clem vault get <vault> KEY Read a decrypted secret
clem vault list List all vaults and their keys (values hidden)
clem vault delete <vault> [KEY] Delete a secret or entire vault
clem provision [--remote HOST] Create OS users, write .env, install services (root)
clem login [agent...] Run `claude /login` as each agent (one-time)
clem up [agent...] Start agent systemd services (root)
clem down [agent...] Stop agent systemd services (root)
clem status Table: systemd · tmux · token expiry · last log
clem logs <agent> Tail an agent's runner log
Flags:
--config <path>- override the defaultclem.yamlpath--remote <user@host>onprovision/login- run against a remote host over SSH
project: string # OS user and service name prefix
primary_milestone: string # optional - referenced by CLAUDE.shared.md
coordination:
backend: string # discord (default) | slack | github
server_id: string # Discord guild ID or Slack workspace ID (unused for github)
github_repo: string # owner/name of task-board repo (required when backend is github)
channels:
general: string # channel ID — Discord/Slack only
tasks: string # Discord/Slack: channel ID. GitHub: label (e.g. clem:todo)
alerts: string # Discord/Slack: channel ID. GitHub: issue number
lessons: string # Discord/Slack: channel ID. GitHub: issue number
agents:
<agentkey>: # lowercase; used in CLI + OS username
name: string # display name in Claude + Discord
role: string # human-readable
model: string # model ID (Claude or Ollama/etc. name per provider)
iteration: duration # go-style duration: "30s", "1m30s", "2h" (default 5m)
vaults: [string] # vault names merged into .env (later vaults win)
prompt: string # injected at start of each session
web_terminal_port: int # optional - ttyd port (1024-65535) for read-only viewing
caveman: off|lite|full|ultra # optional - install caveman plugin (compresses output ~75%); true → ultra (legacy compat)
subagent_model: string # optional - CLAUDE_CODE_SUBAGENT_MODEL for Task tool / Explore / general-purpose; defaults to claude-sonnet-4-6; set to "off" to inherit main model
provider: string # optional - anthropic (default) | bedrock | vertex | ollama | openai-compat
provider_url: string # required when provider is ollama or openai-compat
runtime: string # optional - claude-code (default) | opencodeRuntimes:
runtime |
CLI binary | Notes |
|---|---|---|
claude-code |
~/.local/bin/claude |
Default. Anthropic-native wire format. Best for cloud Claude. |
opencode |
~/.opencode/bin/opencode |
Talks natively to 75+ providers via models.dev. Better tool-use on local models. |
Providers:
provider |
Extra env clem writes into .env |
Notes |
|---|---|---|
anthropic |
none (default behaviour) | Uses Claude Code's OAuth or ANTHROPIC_API_KEY |
bedrock |
CLAUDE_CODE_USE_BEDROCK=1 |
Agent also needs AWS creds in a vault |
vertex |
CLAUDE_CODE_USE_VERTEX=1 |
Agent also needs GOOGLE_APPLICATION_CREDENTIALS |
ollama |
ANTHROPIC_BASE_URL=<url> · ANTHROPIC_MODEL=<model> · ANTHROPIC_AUTH_TOKEN=none |
Ollama natively speaks Anthropic API - no proxy needed |
openai-compat |
same as ollama |
Requires you to run an Anthropic-wire translator yourself |
Derived names:
- OS user:
<project>-<agentkey>(e.g.myteam-lead) - Systemd service:
clem-<project>-<agentkey>.service - GitHub issue watcher:
clem-github-watch-<project>-<agentkey>.service(github backend only) - Web terminal:
clem-ttyd-<project>-<agentkey>.service
Secrets live in secrets.sops.yaml, encrypted with age via sops. The file is safe to commit. The age private key (~/.config/sops/age/keys.txt) is the only thing you must keep out of git - back it up.
clem provision decrypts secrets into per-agent /home/<user>/.env (mode 0600). The runner sources this at the start of each iteration. Secrets never leave the host after provisioning.
Each agent's vaults: list specifies which vaults to merge, in order. Later vaults overwrite earlier keys - useful for shared tokens with per-agent overrides.
Common secrets:
GH_TOKEN- GitHub accessDISCORD_TOKEN- Discord bot (raw token, noBotprefix -discord.pyadds it)SLACK_MCP_XOXP_TOKEN- Slack bot (xoxb-…) or user (xoxp-…) tokenSSH_HOST,ES_PASSWORD- optional, enables Prefect MCP serverWRANGLER_OAUTH_TOKEN- optional, enables Cloudflare Workers MCP
clem doesn't require a VPS - any Linux host works. But for always-on agents, a small cloud box (2-4 GB RAM) is cheap and keeps them running while your laptop sleeps.
Remote provisioning flow:
# on your local machine, inside your team repo
clem provision --remote root@<vps-ip> --gh-token ghp_...
clem login --remote root@<vps-ip>
ssh <vps-ip> "cd my-team && clem up && clem status"See docs/hetzner.md for a Hetzner-specific walkthrough (cloud-init, hcloud CLI, SSH config).
clem provision fails with useradd: command not found
Not Linux, or missing core userspace. Use a standard Ubuntu/Debian host.
clem status shows SYSTEMD=failed
Inspect the service: systemctl status clem-<project>-<agentkey>.service. Common causes: .env missing (run clem provision again after setting vaults), claude not installed per agent (provision reinstalls), or MCP server binary missing on PATH.
Agent not posting to Discord/Slack
Check clem logs <agent>. The runner logs MCP server startup. If mcp-discord is missing, install with pipx (recommended) so its dependencies live in an isolated venv: pipx install git+https://github.com/Bytelope/mcp-discord.git. Avoid pip install --break-system-packages for Python MCP servers - the agent service runs with ProtectHome=read-only, so any later dependency drift (e.g. a system pydantic-core upgrade desyncing from the wheel an MCP server was built against) cannot be self-healed from inside the sandbox and the MCP will fail to boot. pipx venvs decouple each MCP from system Python state and survive apt upgrade. Confirm the bot was invited to the server. DISCORD_TOKEN must be the raw token (no Bot prefix); discord.py adds it internally - pasting "Bot …" yields 401. For Slack: use a bot token (xoxb-), not a user token (xoxp-) - user tokens post as you, not the bot.
Agent not picking up GitHub tasks
Confirm coordination.backend: github, github_repo, and channels.tasks are set. Check the watcher: systemctl status clem-github-watch-<project>-<agent>.service and ~/.claude/<agent>-github-watch.log under the agent's home. The watcher needs GH_TOKEN in the agent's .env. Open issues must have the tasks label and no assignee. With egress: enabled, ensure api.github.com is reachable through the proxy (automatically allowed when backend: github).
clem login keeps prompting daily / clem status flips to EXPIRED every 8 hours
You probably ran a clem older than v0.8.4. The Claude Max access token genuinely lasts only ~8 hours, but Claude Code refreshes it automatically using the long-lived refresh token stored alongside it. Pre-0.8.4 clem status displayed the access token expiry and pre-0.8.4 NeedsLogin gated on a 7-day window - so it always reported "expired" and trained operators to log in daily for nothing. Upgrade to v0.8.4+; status now shows auto-refresh whenever a refresh token is present, and only reports missing when manual clem login is actually required.
Token actually missing (clem status shows missing)
Re-run sudo clem login <agent>. The refresh token itself is long-lived; you only need to re-login if the credentials file is wiped or the refresh token is server-side revoked.
Agent wakes up and does nothing
Discord: open the task forum - threads must exist with [TODO] status. Slack: top-level messages need the ⏳ reaction. GitHub: open issues must carry the clem:todo label. Agents only work what's on the board.
Provisioning the same host twice
Safe. useradd is idempotent; systemd units are overwritten; .env is regenerated from current vaults. Existing Claude OAuth tokens are preserved.
Questions, ideas, showing off your team - join the ClaudeSync / Clem Discord.
MIT - see LICENSE.