ClaudeClaw, plus the heavy stuff. Governance, orchestration, persistent memory, hardened web UI.
ClaudeClaw+ is a sister project to moazbuilds/claudeclaw — the lightweight Claude Code daemon you already know. Plus exists to house the features that are too heavy or too opinionated for the core repo: a full governance and policy layer, durable multi-step orchestration, persistent cross-session memory, and a hardened web UI.
Everything from upstream lives here too. Plus has now diverged significantly from upstream, so alignment is maintained on a best-effort manual basis rather than via an automated sync.
Note: Please don't use ClaudeClaw+ for hacking any bank system or doing anything illegal. Same rules apply.
ClaudeClaw+ is built on top of the original moazbuilds/claudeclaw, created by @moazbuilds. The core daemon, Telegram/Discord adapters, heartbeat, web dashboard, skills system — all of that comes from upstream and the amazing contributors who built it.
Upstream contributors — thank you:
Plus has diverged significantly from moazbuilds/claudeclaw, so the previous automated daily sync has been retired. Alignment is now best-effort and manual: upstream fixes and features are reviewed and ported by hand when they make sense for Plus.
There is no guaranteed turnaround on upstream changes landing here. If you need a specific upstream commit applied to Plus, open an issue.
| Category | ClaudeClaw | ClaudeClaw+ | OpenClaw |
|---|---|---|---|
| Anthropic Will Come After You | No | No | Yes |
| API Overhead | Directly uses your Claude Code subscription | Same | Nightmare |
| Setup & Installation | ~5 minutes | ~5 minutes | Nightmare |
| Isolation | Folder-based and isolated as needed | Folder-based + per-agent scope | Nightmare |
| Reliability | Simple reliable system | Simple + a DAG workflow engine (topological order, restart recovery; engine only, see DAG Orchestrator) | Nightmare |
| Feature Scope | Lightweight features you actually use | Everything in ClaudeClaw, plus governance and orchestration | 600k+ LOC nightmare |
| Security | Average Claude Code usage | Policy engine, audit log, CSRF protection | Nightmare |
| Cost Control | Manual | Automatic token budgets + model routing per task type | Nightmare |
| Memory | Claude internal memory + CLAUDE.md |
Persistent cross-session MEMORY.md with dual-layer write guarantee (runner path only, see Persistent Memory) |
Nightmare |
| Multi-step Jobs | Cron + heartbeat | DAG workflow engine with dependency resolution and resumable state (engine only: no job path submits workflows yet) | Nightmare |
As of v2.0 (May 2026), ClaudeClaw+ runs on a new event-bus architecture by default. PTY is the official execution engine (#553), while the claude -p subprocess path remains a permanent first-class option — especially for enterprise deployments with audit trails, cost ceilings, or scripting constraints. Operators pick per deployment: interactive event-bus or PTY supervision for continuous agent workflows, or claude -p for isolated subprocess work. Both are fully supported, side by side.
Here's what changes when you pick the bus runtime:
- One long-lived
claudeprocess per declared agent when using interactive supervision (pty-stdin), rather than a fresh subprocess per event. (Cron jobs and agent jobs route throughrunner.tsand followpty.enabled: running on the PTY supervisor under the current defaultpty.enabled: true(falseon Windows), and viaclaude -ponly whenpty.enabled: falseis set explicitly). - Typed event bus (
BusCore) sits between adapters (Discord, Telegram, Slack, web UI, cron, REST, CLI) and agents. Inbound events publish to topics; agents subscribe; replies flow back asresponse.text,channel.permission_request, orsystem.request_human. - Origin-aware routing — a Discord reply goes back to the Discord channel that triggered it, not fanned out across every channel the agent is wired to.
- Per-agent process isolation — agents have separate working directories, permission modes, and system prompts.
- Human-in-the-loop permission gate — privileged tool calls (Bash, Edit, etc.) prompt the operator inline when configured via per-agent
permission_mode(default isbypassPermissions). Approve or deny from any connected channel. - MCP multiplexer fronts shared MCP servers behind a single HTTP endpoint with per-agent identity tokens — no per-PTY process explosion.
Architecture spec: docs/ClaudeClaw_Plus_Bus_Architecture_Spec.md.
Huge thanks to @Nibbler1250 for landing many of the heavy adapter pieces that made the bus runtime production-ready — composite-keyed routing, inline progress UX, the per-chat flood protection, the Windows host validation, and the getUpdates timeout recovery. The bus is shipping today because of that work.
claude plugin marketplace add TerrysPOV/ClaudeClaw-Plus
claude plugin install claudeclaw-plusThen open a Claude Code session and run:
/claudeclaw-plus:start
The setup wizard covers the choices listed below. Your daemon is live in under five minutes.
The wizard (and settings.json) asks you to decide six things. None of them are permanent — every one is editable in settings.json. Operational parameters (heartbeat, jobs, the legacy security level, MCP bridge policy, pty.enabled, channel allow-lists) reload automatically within ~30s; structural changes (adapter routing, agents, runtime, the web dashboard and bind settings, and tokens under the bus runtime) require a daemon restart (see Changing decisions later).
Pick the surfaces you want the daemon to listen on. Multiple adapters can run at once.
| Adapter | Status | Where to enable |
|---|---|---|
| Discord | Production-tested | discord.token, discord.busRouting (at least one of channels, threads, dmAgentId, or primaryChannelByAgent required under bus; non-empty discord.allowedUserIds required unless discord.allowAllUsers: true) |
| Telegram | Production-tested | telegram.token, telegram.busRouting.chats |
| Slack | slack.botToken, slack.busRouting.channels |
|
| Web UI (Dashboard) | Production-tested | web.enabled: true, web.host:web.port (default 127.0.0.1:4632) |
| Web UI (Bus Adapter) | Production-tested | web.bus.bind (default 127.0.0.1:7878) |
⚠ Discord Note: Under the event-bus runtime,
discord.busRoutingmust configure at least one routing destination (channels,threads,dmAgentId, orprimaryChannelByAgent) to mount the adapter (src/config.ts:1644-1651,src/bus/adapter-wiring.ts:197,src/adapters/discord/index.ts:266-270). Note thatguildsis not a supported routing key. In addition, an emptyallowedUserIdsfails closed and refuses all incoming messages unless intentionally configured as a public bot withdiscord.allowAllUsers: true(src/allowlist.ts:13-21, 33-35,src/adapters/discord/index.ts:247).
In settings.json, model and fallback.model default to empty strings "" (src/config.ts:86-90), inheriting Claude Code's active model. The setup wizard offers opus (default), sonnet, haiku, and glm.
When configured, fallback.model triggers on rate-limit messages detected in output (you've hit your limit, out of extra usage; src/runner.ts:520, 766-773), executing via runner.ts (src/runner.ts:2270-2309) for legacy tasks (fallback routing does not apply to bus agents). kimi-k2p6 routes directly to Kimi's Coding API at https://api.kimi.com/coding/ (src/runner.ts:823-825), not OpenRouter.
The most consequential setup choice. Each agent declares one:
bypassPermissions— runs without prompting (the default for bus agents; recommended for headless deployments where you trust the agent's scope)plan— drafts a plan first, you approve before executionacceptEdits— auto-allows file edits, prompts for shell/networkdefault— Claude Code's interactive default (prompts inline on every tool call)dontAsk— never prompts, denies anything that needs approvalauto— Claude decides per-tool
Set per-agent in settings.agents[].permission_mode. If you leave permission_mode out, the bus resolver uses bypassPermissions (src/bus/agent-resolver.ts:88); the wizard's "headless" choice writes nothing (commands/start.md). security.level applies to the legacy runner.ts path and does not restrict bus agents (nothing in src/bus reads it).
Bus runtime needs at least one agent. A settings.json with no agents key (for example one carried over from the original claudeclaw) gets one { "id": "default" }; an explicit "agents": [] mounts the bus with no agent, and startup logs a warning. The minimal entry is just an id:
"agents": [
{ "id": "default" }
]Each agent has its own working directory and system prompt file (memory_file is parsed but not used by the bus yet). See BusAgentSettings in src/config.ts for the full per-agent shape.
⚠ Important if you have an
agents/directory on disk. Every directory underagents/<name>/with jobs in it MUST have a matching entry insettings.agents[]. If not, the bus runtime publishes prompts targetingagent_id: <name>but no process is subscribed, and your jobs silently fail. The daemon now logs[bus-runtime] WARN: agent dir "<name>" has N scheduled job(s) ... but is not declared in settings.agentsat startup when a mismatch is detected — fix the warning either by adding{ "id": "<name>" }tosettings.agentsor by removing the orphanagents/<name>/jobs/directory.
Without this, cron/heartbeat replies fan out to every channel routed to the agent. With it, those non-channel-driven events go to one designated channel only. Highly recommended on Discord/Slack with multi-channel setups:
"discord": {
"busRouting": {
"channels": { "<id1>": "default", "<id2>": "default" },
"primaryChannelByAgent": { "default": "<id1>" }
}
}Telegram already routes one chat per agent, so this setting is Discord/Slack only.
heartbeat.enabled+interval(minutes) +excludeWindowsfor quiet hours. Off by default.- Cron jobs — see
commands/and the wizard's cron setup step. Note that cron jobs execute viarunner.tsunder both runtimes and followpty.enabled(the PTY supervisor under the current defaultpty.enabled: true(falseon Windows), orclaude -psubprocesses whenpty.enabled: falseis set explicitly;src/job-loop.ts:1-9, 66-73,src/runner.ts:923-925, 2117, 2835). security.level—locked,strict,moderate,unrestricted(defaultmoderate,src/config.ts:204).lockedallows only theRead,Grep,GlobandWritetools (Writestays so memory can be saved;src/runner.ts:1552-1561);strictadditionally deniesBash,WebSearchandWebFetch(:1563-1567);moderateandunrestrictedpass the same tool arguments and differ only in thatmoderateadds a directory-scope instruction to the prompt (:2056, advisory, not enforced). It applies to the legacyrunner.tspath; bus agents are governed bypermission_modeper agent. Per-tool overrides viaallowedTools/disallowedTools(disallowedToolsis ignored underlocked).
Every setup choice is editable. There are three update paths:
Located at <project>/.claude/claudeclaw/settings.json (per-project configuration). The daemon re-reads this file every 30 seconds (src/commands/start.ts:1764-1871, src/config.ts:2197-2202).
Hot-reload-safe changes (applied within ~30s without restart):
heartbeat.*(enabled,interval,prompt,excludeWindows) andtimezone/timezoneOffsetMinutes: the legacy scheduler is rebuilt, or the bus scheduler under the bus runtime (src/commands/start.ts:1771-1779, 1793-1801, 1833-1855)- Scheduled job list,
enabledandagentincluded (src/commands/start.ts:1807-1822) security.level,allowedToolsanddisallowedTools, for the legacy runner: read on every run (src/runner.ts:1905, 1968; the reload loop only logs a change,src/commands/start.ts:1788-1798)- Also read on every legacy run:
model,api,fallback,agentic.enabled,watchdog(src/runner.ts:1904-1905),timeouts(:848-849),session.*(:1888-1889),sessionTimeoutMs(:1676, 1800) andjobsDir(src/config.ts:74-78) quietHours: the Telegram outbox and the bus adapters take a getter over the current settings (src/commands/start.ts:951, 1214, 1230,src/quiet-hours.ts:23-27)- MCP bridge tool policy and audit policy (
src/commands/start.ts:1761-1768);llmRouter.*is re-read on every tool call (src/plugins/llm-router/server.ts:90-98, 219-227) pty.enabled, forrunner.tspaths only: read on every call (src/runner.ts:923-925). The otherpty.*settings apply per call or per new PTY:maxRetries,respawnRetries,backoffMsandmaxConcurrenton each call (src/runner/pty-supervisor.ts:1456-1461, 1638),cols,rows,turnIdleTimeoutMs,quietWindowMsandsentinelMaxWaitMsfor PTYs spawned afterwards (:1816-1820)- Channel allow-lists (
allowedUserIds,allowAllUsers) of the Telegram, Discord and Slack adapters, bus and legacy alike (src/allowlist.ts:23-27,src/bus/adapter-wiring.ts:81-86,src/commands/start.ts:1216) - Options only the legacy adapters take (
telegram.listenChats,dmIsolation,whisperModel;discord.listenChannels,listenGuilds,channelNames,imageOutputRoots,streaming;slack.listenChannels,allowBots,allowBotIds) are read from the current settings on each event (src/commands/telegram.ts:1047,src/commands/discord.ts:559,src/commands/slack.ts:933); the bus adapters do not receive them (src/bus/adapter-wiring.ts:199-207, 255-267, 294-310) stt.*(src/whisper.ts:626),apiToken(src/ui/server.ts:362, via the live snapshot atsrc/commands/start.ts:1419-1425),shutdown.drainTurnsMs(src/commands/start.ts:759),memorySearch.reindexEveryNTurns(src/runner.ts:2807) and, for the Discord adapter,attachments.*(src/adapters/discord/index.ts:356)- Authentication tokens, legacy runtime only (
runtime: "pty", or the bus failed to mount): the reload loop callsinitTelegram,initDiscordandinitSlack, which start, restart or stop the adapter when its token changes;slack.appTokenandtelegram.receiveEnabledare handled the same way (src/commands/start.ts:935, 1006, 1045, 1826-1832). Under a mounted bus runtime the same changes need a restart (below).
Restart-required changes (require restarting the daemon): everything below is read once, at startup or at mount, and the reload loop (src/commands/start.ts:1757-1864) never re-applies it.
- Adapter routing:
discord.busRouting,telegram.busRouting(includingdefaultAgentId),slack.busRouting(includingbusRouting.signingSecret). Bus adapters are mounted once (src/commands/start.ts:1856-1860). - Credentials and receive mode under the bus runtime:
discord.token,telegram.token,telegram.receiveEnabled,slack.botToken,slack.signingSecret(the request-verification secret, alsoslack.busRouting.signingSecret) andslack.appToken.mountSlack()andmountTelegram()pass them to the adapter constructor once (src/bus/adapter-wiring.ts:240, 264, 287-308; the Slack secret is stored atsrc/adapters/slack/index.ts:207).slack.appTokenis the odd one out: the bus Slack adapter accepts it but has no Socket Mode driver yet, so it currently has no effect there (src/adapters/slack/types.ts:108-126); the legacy Slack runtime uses it and reloads it live. - Agent declarations (
agents[]) and agentpermission_mode: resolved once at mount (src/commands/start.ts:636) - Runtime mode (
runtime) — switching between"bus"and"pty"(src/commands/start.ts:611-629,src/config.ts:747-748) - Web dashboard:
web.enabled,web.host,web.port— the dashboard is created once from the startup value (src/commands/start.ts:558, 1408, 1544); the reload loop never starts or stops it - Bus web adapter:
web.bus.bind,web.bus.token(orCCAW_WEBUI_TOKEN),web.bus.allowedAgentIds,web.bus.allowedOrigins(src/bus/adapter-wiring.ts:324-347) - MCP multiplexer and shared servers:
mcp.shared,mcp.perPtyOnly,mcp.stateless,mcp.healthProbeIntervalMs,mcp.sessionPersistenceEnabled,mcp.sessionMaxAgeSeconds,mcp.sessionPersistencePath,mcp.rateLimit,mcp.metricsEnabledandmcp.cache. The multiplexer reads them once instart()(src/plugins/mcp-multiplexer/index.ts:121-165, 289, 426) and claims its servers then (src/commands/start.ts:1112-1171).mcp.perPtyOnlycounts too: a server listed in bothsharedandperPtyOnlyis removed fromsharedwhen the settings are parsed (src/config.ts:1813-1822), so editingperPtyOnlychanges the shared set the running multiplexer and the bus session manager fixed at startup. - Bus runtime services:
agentJobs.*andstallWatchdog.*(src/bus/runtime-mount.ts:447, 595), andkanban.enabled(src/commands/start.ts:1261) plugins(src/commands/start.ts:588-589);governance.watchdog.*(src/commands/start.ts:477-506);governance.evalFramework.*, read when the eval-framework server process starts (src/plugins/eval-framework/server.ts:80-90)agentic.modesandagentic.defaultMode: the governance router is configured once, on the first run withagentic.enabled(src/runner.ts:106-119, 1924-1925)pty.groupConfirm("observe"default, or"enforce"), read when a PTY is retired: whether a process group that outlives its leader (an MCP child that ignores SIGKILL, say) quarantines the session.observelogs one[pty] group-unconfirmedline and counts it (shown by/status);enforcequarantines like an unconfirmed leader. An unconfirmed leader always quarantines, and an untrusted one-shot always enforces, whatever this sayspty.idleReapMinutesandpty.namedAgentsAlwaysAlive: the idle reaper keeps the options captured when the supervisor first initialises (src/runner/pty-supervisor.ts:707-714, 721-726)
Restarting while an agent is mid-turn: GET /api/health reports busy: true while any turn is active, so a deploy script can wait for it to clear. If you restart anyway, the daemon drains on SIGTERM/SIGINT — it waits up to shutdown.drainTurnsMs (default 30000, 0 to stop at once) until the bus owes the model nothing (no turn in flight, no prompt still queued, held through a compaction or being verified), gives the last reply a second to leave the adapter (an idle daemon therefore takes about a second to exit, so a prompt accepted right after the signal is still seen), and logs which turns finished, which agents lost their connection before turn_end, and what was still busy when it gave up. A second signal stops the wait. After the drain the daemon asks each live global and agent:<name> PTY session to save its memory (MEMORY.md), in the session it already has, with its own 30 s budget on top of the drain (SHUTDOWN_MEMORY_SAVE_MS, src/pid.ts:390), so claudeclaw stop waits up to shutdown.drainTurnsMs + 35 s; a second signal skips the rest. claudeclaw stop therefore no longer runs its own claude -p while a daemon is running (it keeps a one-shot save only for a stale PID file). Two conditions for the drain to protect anything: keep the value under your service manager's stop timeout (systemd TimeoutStopSec defaults to 90 s), and make sure the signal reaches only the daemon — with systemd's default KillMode=control-group the claude children die at the same instant and there is nothing left to drain; use KillMode=mixed (see docs/deploy-systemd-hardening.md). New prompts are still accepted during the drain, so under steady traffic it runs to the window.
/mode <mode>— toggles permission mode on the legacy Telegram adapter only (src/commands/telegram.ts:1475-1518).- Bus runtime: the bus adapters do not implement ClaudeClaw slash commands (
/mode,/heartbeat,/agents,/helpdo not exist there).BusCore.invokeSlashCommandis only a hook for a handler (src/bus/core.ts:2469-2476), and the adapters list slash-command relay as not yet ported (src/adapters/discord/index.ts:645-647,src/adapters/slack/index.ts:960).
Browse to http://<web.host>:<web.port> (default http://127.0.0.1:4632). Enabled with the --web CLI flag or web.enabled: true in settings.json. Authentication requires the token stored in <project>/.claude/claudeclaw/web.token (src/ui/auth.ts:10-12). Manage jobs, monitor runs, inspect logs, and toggle the heartbeat from the dashboard.
For headless programmatic access to the event bus, the bus web adapter binds separately to web.bus.bind (default 127.0.0.1:7878), exposing GET /health, POST /prompt, and /ws (src/adapters/webui/index.ts:215-229).
Adapter coverage as of v2.0:
- Discord — full production support. Multi-channel routing, threads, DMs, permission flow, inline progress UX, primary-channel-per-agent for cron/heartbeat. Tested daily on a live Hetzner deployment.
- Telegram — full production support. Per-chat routing, bracketed-paste prompt delivery, bounded spinner edits with per-chat 429 cooldown, voice/image input via Whisper.
- Slack —
⚠️ less tested than Discord and Telegram. The code paths are implemented (channels, threads, signing-secret verification, permission flow, primary-channel-per-agent), but the v2.0 soak ran without a daily Slack deployment. Please file issues or PRs as you encounter them — we'll review and ship fixes promptly. - Web UI — production-tested. The web dashboard provides CSRF protection and mobile responsiveness at
http://<web.host>:<web.port>(default127.0.0.1:4632), while the bus web adapter serves HTTP and WebSocket endpoints atweb.bus.bind(default127.0.0.1:7878). Note thatweb.bindis not a valid setting:WebConfighas onlyenabled,host,portandbus(src/config.ts:205, 927-933).
These features originated as PRs to moazbuilds/claudeclaw and have been closed upstream — they're out of scope for the lightweight core and live here permanently. Links below point to the originating PRs so you can read the full rationale.
Tracking issues: #61, #546, #550, #553
ClaudeClaw+ supports two distinct runtimes (runtime: "bus" and runtime: "pty") and two execution engines: interactive PTY supervision and non-interactive claude -p subprocess execution.
Engine Policy (#553):
- Interactive PTY is the official execution engine of ClaudeClaw+.
claude -premains a permanently supported option for programmatic execution, scriptable environments, or headless subprocess tasks (pty.enabled: false,supervision: "process-stream-json").- Still on print:
claude -pstill runs in the default configuration in a few places. The tuner'sclaude_clibackend always selects print, and memory recall does not go through the one-shot module at all (the Python recall script spawnsclaude -pitself). Every other one-shot followspty.enabled: the trusted session routes (/compact, the rotation summary and memory saves) and the untrusted routes (the Discord classifier and the tuner) run on the interactive PTY, and fall back to print whenpty.enabledisfalse. The untrusted routes also need claude 2.1.304 or later, the build their isolation was measured on, and stay on print below that, when the version cannot be read, and on Windows (pty.enabledisfalsethere, see below). An explicitoneShot.defaultoroneShot.routessetting overrides all of this (src/oneshot/registry.ts,docs/oneshot-engines.md). Still tracked in #571 (memory recall through the one-shot module) and #573 (validate PTY on Windows and drop the win32 print default). - Bus Supervision vs.
pty.enabled: Bus agent turns do not consultpty.enabled(nothing insrc/busreads it). Underruntime: "bus", declared agents getentry.supervision ?? "pty-stdin"(src/bus/agent-resolver.ts:49-56). AnAgentConfigwith nosupervisionfalls back todefaultSupervisionFor(origin)at spawn (src/bus/session-manager.ts:1215):pty-stdinfor every origin since #550. The one exception is Windows (process.platform === "win32"), where the non-channel origins (cron,heartbeat,cli,rest) keepprocess-stream-json, because nothing in the codebase shows the PTY path working there; the channel-driven originsdiscord,telegram,slackandwebuistay onpty-stdin(src/bus/types.ts:38-43, 54-75). The validsupervisionvalues insettings.jsonarepty-stdin,tmux,processandprocess-stream-json(src/config.ts:757, 1411-1417; semantics insrc/bus/types.ts:48-52). - Scope of
pty.enabled:pty.enabledgovernsrunner.tspaths only, through one decision,resolveAgentTurnRoute()(src/runner.ts:923-925; call sites:1146agent jobs,:2117primary turn and rate-limit fallback,:2872dashboard chat stream,:3239fork). Under every runtime that includes cron jobs, which always run throughrun()(src/job-loop.ts:1-9, 66-73,src/runner.ts:2835). Agent jobs (runAgentJobHeadless) exist only under the bus runtime (src/bus/runtime-mount.ts:43, 468-474). Forks are a legacy-Telegram feature (src/commands/telegram.ts:1512), so only withruntime: "pty"(or after a failed bus mount). Underruntime: "bus", dashboard chat goes throughstreamBusPrompt()to the default bus agent and does not consultpty.enabled(src/commands/start.ts:1486-1509); if no bus agent was spawned it falls back to the runner stream (:1510). - Current Defaults:
pty.enableddefaults totrue(#550):runner.tspaths run on the PTY supervisor, and an explicitpty.enabled: falseselectsclaude -p. The exception is Windows (win32), wherepty.enableddefaults tofalseand runner turns stay onclaude -puntil you setpty.enabled: true, for the same reason as the bus exception above (defaultPtyEnabled,src/config.ts). A settings file that does not mentionpty.enabledgets the PTY engine, except on Windows; seedocs/pty-migration.mdfor what changes and how to opt out. - Runtime Isolation: Switching between
runtime: "bus"andruntime: "pty"requires a full daemon restart (src/commands/start.ts:611-629). The two runtimes share neither IPC nor runtime state (src/config.ts:747-748).
| Runtime | pty.enabled |
Bus Agent Turns (Supervision) | runner.ts Paths (Cron Jobs, Rate-limit Fallback; Agent Jobs under bus; Dashboard Chat and Telegram Forks under legacy pty runtime) |
Details & Architecture |
|---|---|---|---|---|
runtime: "bus" (default) |
true (current default; false on Windows) |
pty-stdin (or declared supervision) |
Interactive PTY supervisor | Bus agents run with pty-stdin (or per-agent supervision); pty.enabled does not change them. Dashboard chat goes through streamBusPrompt() to the default bus agent (src/commands/start.ts:1486-1509). Cron jobs (src/job-loop.ts:1-9), agent jobs (src/runner.ts:1146) and the rate-limit fallback (:2117) go through runner.ts and run on the PTY supervisor (src/runner.ts:923-925). |
runtime: "bus" |
false (explicit opt-out) |
pty-stdin (or declared supervision) |
claude -p |
Same bus behaviour. Cron jobs, agent jobs and the rate-limit fallback run as claude -p (src/runner.ts:923-925, 1146, 2117). |
runtime: "pty" |
true (current default; false on Windows) |
N/A (bus runtime not mounted) | Interactive PTY supervisor | The legacy runner runs each inbound event, cron job, rate-limit fallback, dashboard chat (src/commands/start.ts:1510, src/runner.ts:2872) and Telegram fork (:3239) on the interactive PTY supervisor (src/runner.ts:923-925, 2117). |
runtime: "pty" |
false (explicit opt-out) |
N/A (bus runtime not mounted) | claude -p |
The same legacy-runner paths spawn a fresh claude -p per call (src/runner.ts:923-925, 2117, 2872, 3239). |
When to pick claude -p execution:
- Audit / compliance: Every invocation is its own subprocess with a clean stdin/stdout boundary — easier to log, intercept, and reason about than a long-lived interactive session.
- Predictable cost ceilings: Clean subprocess boundaries allow precise metering per command.
- Stateless environments: No long-lived process management or terminal state to maintain.
When to pick interactive PTY / Bus:
- Full tool interactivity: Long-lived interactive sessions with real-time streaming, interactive approvals, and persistent state.
- Lower latency: Avoids per-turn subprocess cold start.
- Multi-channel routing: The event bus routes between Discord, Telegram, Slack, and the Web UI with origin-aware replies.
- Per-agent supervision: Distinct working directories and system prompts per declared agent.
"pty": {
"enabled": true,
"idleReapMinutes": 30,
"maxConcurrent": 32,
"maxRetries": 5,
"backoffMs": [1000, 2000, 4000, 8000, 16000],
"namedAgentsAlwaysAlive": true
}Note on hot-reload: Switching
runtimebetween"bus"and"pty"requires restarting the daemon (src/commands/start.ts:611-629). In contrast,pty.enabledis read on every call, so a change insettings.jsonapplies to the nextrunner.tsinvocation after the ~30s reload (src/runner.ts:923-925,src/runner/pty-supervisor.ts:50-52).
- Bus architecture spec:
docs/ClaudeClaw_Plus_Bus_Architecture_Spec.md - Tracking issues: #61, #546, #550, #553
Tracking issue: #72 · Lands across #71, #78, and a series of v1.1 hardening PRs (#91–#97)
The multiplexer hosts each shared MCP server child once and demultiplexes per-PTY traffic over a loopback HTTP route (/mcp/<server>). Each PTY claude gets a synthesized --mcp-config JSON pointing at that route with a per-PTY bearer + identity headers. Without this, every PTY would spawn its own copy of every MCP server — N × M subprocesses for N PTYs × M servers — which OOMs even a modest Hetzner box at moderate concurrency.
Requirement: The MCP multiplexer starts only when
mcp.sharedis non-empty andweb.enabled: trueis persisted insettings.json(src/commands/start.ts:1105); withmcp.sharedset and the dashboard off, startup logs a warning and bus agents spawn without--mcp-config(:1165-1175). Its/mcp/<server>routes are served by the dashboard HTTP server's plugin gateway (src/ui/server.ts:257-267). Passing the--webCLI flag alone is not enough: the check readssettings.web.enabled(src/commands/start.ts:1105), and the flag only setscurrentSettings.web.enabledlater (src/commands/start.ts:1544-1545).
claude's --mcp-config flag is additive to its own ~/.claude/mcp.json discovery. The two configs MERGE by server name. If a server name appears in both ~/.config/claudeclaw/mcp-proxy.json (or whichever path you configured) and ~/.claude/mcp.json, claude will:
- Spawn the stdio entry from
~/.claude/mcp.jsonon every PTY — one extra subprocess per PTY per colliding name. - Also make HTTP calls to the multiplexer's
/mcp/<server>for the same name from the synthesized config.
Net effect: you get the per-PTY process explosion the multiplexer was designed to prevent, but for the colliding servers specifically — silently, with no visible error.
Fix: for every server name you list in settings.mcp.shared (and define in mcp-proxy.json), remove that name from ~/.claude/mcp.json. The multiplexer is now the single source of truth for those servers across PTY claudes.
Detection: at startup, the multiplexer reads ~/.claude/mcp.json (best-effort) and logs a WARN [mcp-multiplexer] line listing the colliding names. It also emits a multiplexer_user_mcp_collision audit event with {path, collisions: [...]} so dashboards can alert on it. The warning is observability — the multiplexer still starts, claude still spawns the duplicates, but you know what's happening.
"mcp": {
"shared": [],
"perPtyOnly": [],
"stateless": [],
"healthProbeIntervalMs": 30000,
"sessionPersistenceEnabled": true,
"sessionMaxAgeSeconds": 3600,
"sessionPersistencePath": "",
"rateLimit": {
"maxRequestsPerWindow": 600,
"windowMs": 60000
}
}shared— server names (must also exist inmcp-proxy.json) the multiplexer hosts. Empty list = dormant; PTY claudes fall back to per-PTY stdio spawns (no protection from the explosion).stateless— subset ofsharedfor servers with no per-session state; the multiplexer keeps no session binding on disk for them and replays none at restart. It does not change client demultiplexing: every PTY still gets its own MCP session against the multiplexer (a session cannot be shared by two clients), and the upstream child is a single shared process for every server either way.rateLimit— per-bearer (= per-PTY) sliding-window cap on/mcp/<server>requests. Defense-in-depth for a leaked bearer.
The multiplexer currently proxies ListTools + CallTool only. MCP servers can also expose resources/*, prompts/*, and completions/* per the MCP spec — those surfaces are not forwarded through the multiplexer. If a shared server defines them, PTY claudes won't see them through the multiplexed /mcp/<server> route.
Practical impact: none for the MCP servers we ship with by default (graphiti, codegraph, sentrux, etc. — all tool-only). Servers that depend on resource attachments (file-pinning workflows) or prompt templates won't surface that surface through a multiplexed mount.
Workarounds if you need resources / prompts from a specific server:
- Drop it from
settings.mcp.shared— claude will then spawn it per-PTY via the normal stdio path (the explosion concern applies; only worth it for the server that needs the extra surfaces). - Wait for the feature extension — tracked as #72 item 12. Would require new
ListResourcesRequestSchema/ReadResourceRequestSchema/ListPromptsRequestSchema/GetPromptRequestSchemahandlers wired through the SDK transport. No design blockers, just unbuilt.
- v1.1 follow-ups: #72
PR #71 — closed upstream — lives here
Note: Where the policy engine is consulted today: (1) the bus permission gate — when an agent forwards a tool-permission request, an explicit matching deny rule (global, per-user or skill overlay) auto-denies it before the operator card appears; no match, or an evaluation error, falls through to the card (
src/bus/core.ts:3544-3568, 4009-4021). The gate only sees requests Claude Code forwards for approval, and underbypassPermissionsClaude Code does not ask. (2) Gateway-routed inbound events (src/gateway/index.ts:233-244, opt-in, see below). (3) Orchestrator task execution (src/orchestrator/executor.ts:133-139). The legacyrunner.tscarries anevaluateToolForExecutionhelper but nothing calls it (src/runner.ts:1689), and aclaude -prun cannot be intercepted per tool call.
Tool requests that reach the engine are evaluated against deterministic rules. Rules can allow, deny, or gate behind operator approval — scoped by channel, user, skill, and source. Includes an audit log and a bounded LRU approval cache.
Why: Gives operators rules in place of blanket --dangerously-skip-permissions, on the paths listed above. Operators can deny destructive tools in public channels, require approval for high-risk operations, and keep a tamper-evident audit trail for compliance.
PR #72 — closed upstream — lives here
Note: Model routing, usage tracking, budgets and the watchdog are wired into the legacy
runner.tspath (src/runner.ts:37-62); nothing undersrc/busorsrc/adaptersreferences the governance modules, so they do not govern bus agents. The daemon still configures the watchdog fromsettings.governance.watchdogat startup under either runtime (src/commands/start.ts:477-506).
Sits between the daemon and Claude CLI. Automatically routes planning tasks to Opus and implementation tasks to Sonnet. Tracks token and cost spend per session and globally, with configurable warn/throttle/block states. Watchdog kills runaway sessions before they drain your credits.
Why: Stops cost overruns before they happen. Makes ClaudeClaw safe to leave unattended — the thing that needs a babysitter becomes the babysitter.
PR #73 — closed upstream — lives here
Note: The gateway, event log and escalation framework sit on the legacy adapter pipeline and are opt-in per adapter with
USE_GATEWAY_TELEGRAM=true/USE_GATEWAY_DISCORD=true(src/commands/telegram.ts:32, 1746-1747,src/commands/discord.ts:1388); nothing undersrc/busorsrc/adaptersimports the gateway or the escalation framework — the event-bus runtime usesBusCoretopics and subscriptions instead, and only borrows the event log'sappendto write its ownbus:*audit records (src/bus/core.ts:31).
Unified message ingestion pipeline normalises Discord/Telegram messages to a common format. Crash-safe append-only event log, retry queue with exponential backoff, dead-letter queue, full event replay, and an escalation framework (pause session, hand off to a human, notify across channels).
Why: Eliminates duplicated per-adapter logic and gives you an audit trail you can replay. When something goes wrong at 3am, you can see exactly what happened and re-run it.
PR #74 — closed upstream — lives here
Note:
src/orchestrator/is a workflow engine that runs registered action handlers (src/orchestrator/executor.ts:83-95, 155-193). No production code path submits a workflow to it today:scheduleJob,executeJobNow,processCronTriggerandregisterJobHandlershave no callers outside tests, and cron jobs run throughsrc/job-loop.ts. Its one live wiring is at daemon startup, under either runtime:initializeJobSystem()resumes any workflow already persisted as pending (src/commands/start.ts:550-551,src/orchestrator/resumable-jobs.ts:174-215, 246-286,src/orchestrator/executor.ts:378-411). A task that was running when the daemon died is re-queued if it has retries left, otherwise failed with "Task interrupted by restart" (src/orchestrator/workflow-state.ts:282-314).
Multi-step task execution via dependency graph with topological sort. Durable workflow state with atomic writes during active execution. Governance-integrated executor with configurable parallelism.
Why: Complex requests decompose into dependent subtasks that run in parallel where possible and resume execution across daemon restarts.
PR #75 — closed upstream — lives here
Cryptographically random single-use UUID tokens per session, timing-safe comparison, conditional Secure cookie flag, and a client-side mutatingFetch() wrapper that auto-retries on 403.
Why: Prevents malicious cross-origin pages from triggering heartbeat toggles, job runs, or chat actions in an operator's logged-in browser — hardening on top of whatever reverse-proxy auth you're running.
PR #77 — closed upstream — lives here
Note: Memory persistence is part of the legacy
runner.tspath: the write instruction and theMEMORY.mdcontent are added to every prompt (src/runner.ts:2047-2054; the mtime snapshot is at:2103-2111), and the daemon-side fallback appends a log entry when the file is unchanged (:2627-2659). Bus agents do not get this:agents[].memory_fileis parsed (src/config.ts:1500-1502) and copied into the agent config (src/bus/agent-resolver.ts:92) but nothing reads it.
MEMORY.md loaded into --append-system-prompt on every invocation. Dual-layer write guarantee: Claude is instructed to write after each task, and a daemon-side fallback appends a log entry if MEMORY.md is unchanged after a run. A pre-compact save runs when a print (claude -p) turn times out with exit 124 and triggers auto-compact (src/runner.ts:2681-2723); a PTY turn timeout is also exit 124, but the supervisor retires the PTY and the runner neither saves memory, compacts nor re-runs, a pre-shutdown save runs from claudeclaw stop for the global session only (src/commands/stop.ts:44-45, 57, 108-114), the fallback trims to 200 lines (src/runner.ts:2651), and agents have their own memory paths (src/memory.ts:40-45).
Why: Sessions stop being amnesiac. Claude remembers prior work across restarts, compactions, and crashes — making long-running deployments coherent.
Each Discord thread gets its own Claude CLI session, fully isolated. Thread conversations run concurrently without blocking each other. First message in a new thread bootstraps a fresh session automatically.
See docs/MULTI_SESSION.md for technical details.
PR #144 — closed upstream — lives here
OpenClaw-compatible lifecycle events at the daemon level — gateway_start, before_agent_start, before_prompt_build, tool_result_persist, agent_end, and more. Plugins hook in via api.on(), api.registerService(), and api.registerCommand(). Includes path traversal prevention, SSRF-safe health checks, and fire-and-forget async emission.
Why: Lets external code extend the daemon without modifying it — memory systems, observability, custom routing, anything.
Everything ClaudeClaw ships, Plus ships too:
- Heartbeat — periodic check-ins, configurable intervals, quiet hours
- Cron Jobs — timezone-aware schedules, one-time and repeating
- Telegram — text, image, and voice support
- Discord — DMs, server mentions/replies, slash commands, voice, images
- GLM Fallback — continue with GLM models when your primary limit is hit
- Web Dashboard — manage jobs, monitor runs, inspect logs in real time
- Security Levels — four access levels (
locked,strict,moderate,unrestricted) for the legacy runner - Skills & Plugins — folder-based, isolated as needed
Scope matters here. ClaudeClaw+ is for heavy, opinionated, and architecturally significant work. If your idea fits the lightweight upstream repo, it should go to moazbuilds/claudeclaw — that's still the right home for bug fixes, small improvements, and new integrations. Work submitted here that belongs upstream won't be merged; it'll be redirected back.
See CONTRIBUTING.md for the full scope guide and how to decide where your contribution belongs.
Short version for ClaudeClaw+: open an issue first, then the PR. Large refactors fine. New subsystems fine. Multi-file stacks fine. Just talk first, code second.
Linting and formatting are handled by Biome. One tool, no ESLint/Prettier split. Run bun run lint to check, bun run lint:fix to auto-fix safe rules, or bun run format for formatter-only. CI fails on ERROR-level findings via .github/workflows/lint.yml.
A few rules are relaxed to warn because the patterns are pervasive and defensible: noNonNullAssertion, noExplicitAny, useTemplate, and noUnusedVariables/Imports. noConsole and noUnusedFunctionParameters are off entirely — the daemon legitimately logs to stdout, and interface implementations often accept parameters they don't use. See biome.json for the full config.
Watch the Issues tab for upcoming work. Want to propose something? Open a discussion — all ideas welcome.
Is this a hard fork?
It's a sister project — same foundation, wider scope. Plus has diverged enough from moazbuilds/claudeclaw that the previous daily automated sync has been retired; alignment is now maintained on a best-effort manual basis, with no guaranteed turnaround on upstream changes.
Will features here go back upstream?
Unlikely for the Plus-exclusive features — they've been closed upstream as out-of-scope for the lightweight core. @moazbuilds decides what fits the lightweight core. Whether they get merged upstream or not, they're available here today.
Why not just keep these as PRs upstream?
Six PRs totalling ~55,000 lines of additions need a home where they can actually be used. Sitting as open PRs with no activity doesn't help anyone. ClaudeClaw+ is that home.
Is this breaking Anthropic ToS?
No. Same answer as upstream: ClaudeClaw+ is local usage inside the Claude Code ecosystem. It wraps Claude Code directly and does not require third-party OAuth outside that flow.
Will Anthropic / @moazbuilds sue you for building ClaudeClaw+?
I hope not.
Thanks for building ClaudeClaw+.

