diff --git a/CHANGELOG.md b/CHANGELOG.md
index e08ecccc7..02912989a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -20,7 +20,7 @@
### Fixes
- CLI/Daemon: add `clawdbot logs` tailing and improve restart/service hints across platforms.
-- Gateway/CLI: tighten LAN bind auth checks, warn on mis-keyed gateway tokens, and surface last gateway error when daemon looks running but the port is closed.
+- Gateway/CLI/Doctor: tighten LAN bind auth checks, warn/migrate mis-keyed gateway tokens, and surface last gateway error when daemon looks running but the port is closed.
- Auto-reply: keep typing indicators alive during tool execution without changing typing-mode semantics. Thanks @thesash for PR #452.
- macOS: harden Voice Wake tester/runtime (pause trigger, mic persistence, local-only tester) and keep transcript logs private. Thanks @xadenryan for PR #438.
- macOS: preserve node bridge tunnel port override so remote nodes connect on the bridge port. Thanks @sircrumpet for PR #364.
@@ -71,6 +71,7 @@
- Gateway/CLI: add daemon runtime selection (Node recommended; Bun optional) and document WhatsApp/Baileys Bun WebSocket instability on reconnect.
- CLI: add `clawdbot docs` live docs search with pretty output.
- CLI: add `clawdbot agents` (list/add/delete) with wizarded workspace/setup, provider login, and full prune on delete.
+- CLI: add non-interactive flags for `agents add`, support `agents list --bindings`, and keep JSON output clean for scripting.
- Discord/Slack: fork thread sessions (agent-scoped) and inject thread starters for context. Thanks @thewilloftheshadow for PR #400.
- Agent: treat compaction retry AbortError as a fallback trigger without swallowing non-abort errors. Thanks @erikpr1994 for PR #341.
- Agent: add opt-in session pruning for tool results to reduce context bloat. Thanks @maxsumrall for PR #381.
diff --git a/docs/cli/index.md b/docs/cli/index.md
index 3196b6ab2..d3b8571be 100644
--- a/docs/cli/index.md
+++ b/docs/cli/index.md
@@ -330,14 +330,21 @@ List configured agents.
Options:
- `--json`
+- `--bindings`
#### `agents add [name]`
-Add a new isolated agent. Runs the guided wizard unless flags are passed; `--workspace` is required in non-interactive mode.
+Add a new isolated agent. Runs the guided wizard unless flags (or `--non-interactive`) are passed; `--workspace` is required in non-interactive mode.
Options:
- `--workspace
`
+- `--model `
+- `--agent-dir `
+- `--bind ` (repeatable)
+- `--non-interactive`
- `--json`
+Binding specs use `provider[:accountId]`. When `accountId` is omitted for WhatsApp, the default account id is used.
+
#### `agents delete `
Delete an agent and prune its workspace + state.
@@ -420,6 +427,7 @@ Notes:
- `daemon status` uses the same URL/token defaults as `gateway status` unless you pass `--url/--token/--password`.
- `daemon status` supports `--no-probe`, `--deep`, and `--json` for scripting.
- `daemon status` also surfaces legacy or extra gateway services when it can detect them (`--deep` adds system-level scans).
+- `daemon status` prints which config path the CLI uses vs which config the daemon likely uses (service env), plus the resolved probe target URL.
- `daemon install` defaults to Node runtime; use `--runtime bun` only when WhatsApp is disabled.
- `daemon install` options: `--port`, `--runtime`, `--token`.
diff --git a/docs/concepts/multi-agent.md b/docs/concepts/multi-agent.md
index c7556a6cc..9627932fa 100644
--- a/docs/concepts/multi-agent.md
+++ b/docs/concepts/multi-agent.md
@@ -19,6 +19,14 @@ An **agent** is a fully scoped brain with its own:
The Gateway can host **one agent** (default) or **many agents** side-by-side.
+## Paths (quick map)
+
+- Config: `~/.clawdbot/clawdbot.json` (or `CLAWDBOT_CONFIG_PATH`)
+- State dir: `~/.clawdbot` (or `CLAWDBOT_STATE_DIR`)
+- Workspace: `~/clawd` (or `~/clawd-`)
+- Agent dir: `~/.clawdbot/agents//agent` (or `routing.agents..agentDir`)
+- Sessions: `~/.clawdbot/agents//sessions`
+
### Single-agent mode (default)
If you do nothing, Clawdbot runs a single agent:
@@ -38,6 +46,12 @@ clawdbot agents add work
Then add `routing.bindings` (or let the wizard do it) to route inbound messages.
+Verify with:
+
+```bash
+clawdbot agents list --bindings
+```
+
## Multiple agents = multiple people, multiple personalities
With **multiple agents**, each `agentId` becomes a **fully isolated persona**:
diff --git a/docs/multi-agent-sandbox-tools.md b/docs/multi-agent-sandbox-tools.md
index f31b43d5f..4c06777a1 100644
--- a/docs/multi-agent-sandbox-tools.md
+++ b/docs/multi-agent-sandbox-tools.md
@@ -248,7 +248,7 @@ After configuring multi-agent sandbox and tools:
1. **Check agent resolution:**
```bash
- clawdbot agents list
+ clawdbot agents list --bindings
```
2. **Verify sandbox containers:**
diff --git a/docs/start/wizard.md b/docs/start/wizard.md
index 233332e16..b20d01a13 100644
--- a/docs/start/wizard.md
+++ b/docs/start/wizard.md
@@ -44,6 +44,8 @@ To add more isolated agents (separate workspace + sessions + auth), use:
clawdbot agents add
```
+Tip: `--json` does **not** imply non-interactive mode. Use `--non-interactive` (and `--workspace`) for scripts.
+
## Flow details (local)
1) **Existing config detection**
@@ -131,6 +133,7 @@ What it sets:
Notes:
- Default workspaces follow `~/clawd-`.
- Add `routing.bindings` to route inbound messages (the wizard can do this).
+ - Non-interactive flags: `--model`, `--agent-dir`, `--bind`, `--non-interactive`.
## Non‑interactive mode
@@ -153,7 +156,12 @@ Add `--json` for a machine‑readable summary.
Add agent (non‑interactive) example:
```bash
-clawdbot agents add work --workspace ~/clawd-work
+clawdbot agents add work \
+ --workspace ~/clawd-work \
+ --model openai/gpt-5.2 \
+ --bind whatsapp:biz \
+ --non-interactive \
+ --json
```
## Gateway wizard RPC
diff --git a/src/cli/program.ts b/src/cli/program.ts
index 7012bcd5e..2aa74307d 100644
--- a/src/cli/program.ts
+++ b/src/cli/program.ts
@@ -48,6 +48,10 @@ import { registerTuiCli } from "./tui-cli.js";
export { forceFreePort };
+function collectOption(value: string, previous: string[] = []): string[] {
+ return [...previous, value];
+}
+
export function buildProgram() {
const program = new Command();
const PROGRAM_VERSION = VERSION;
@@ -553,9 +557,13 @@ Examples:
.command("list")
.description("List configured agents")
.option("--json", "Output JSON instead of text", false)
+ .option("--bindings", "Include routing bindings", false)
.action(async (opts) => {
try {
- await agentsListCommand({ json: Boolean(opts.json) }, defaultRuntime);
+ await agentsListCommand(
+ { json: Boolean(opts.json), bindings: Boolean(opts.bindings) },
+ defaultRuntime,
+ );
} catch (err) {
defaultRuntime.error(String(err));
defaultRuntime.exit(1);
@@ -566,14 +574,28 @@ Examples:
.command("add [name]")
.description("Add a new isolated agent")
.option("--workspace ", "Workspace directory for the new agent")
+ .option("--model ", "Model id for this agent")
+ .option("--agent-dir ", "Agent state directory for this agent")
+ .option("--bind ", "Route provider binding (repeatable)", collectOption, [])
+ .option("--non-interactive", "Disable prompts; requires --workspace", false)
.option("--json", "Output JSON summary", false)
.action(async (name, opts, command) => {
try {
- const hasFlags = hasExplicitOptions(command, ["workspace", "json"]);
+ const hasFlags = hasExplicitOptions(command, [
+ "workspace",
+ "model",
+ "agentDir",
+ "bind",
+ "nonInteractive",
+ ]);
await agentsAddCommand(
{
name: typeof name === "string" ? name : undefined,
workspace: opts.workspace as string | undefined,
+ model: opts.model as string | undefined,
+ agentDir: opts.agentDir as string | undefined,
+ bind: Array.isArray(opts.bind) ? (opts.bind as string[]) : undefined,
+ nonInteractive: Boolean(opts.nonInteractive),
json: Boolean(opts.json),
},
defaultRuntime,
diff --git a/src/commands/agents.add.test.ts b/src/commands/agents.add.test.ts
index 8d4125528..aadef7258 100644
--- a/src/commands/agents.add.test.ts
+++ b/src/commands/agents.add.test.ts
@@ -55,4 +55,20 @@ describe("agents add command", () => {
expect(runtime.exit).toHaveBeenCalledWith(1);
expect(configMocks.writeConfigFile).not.toHaveBeenCalled();
});
+
+ it("requires --workspace in non-interactive mode", async () => {
+ configMocks.readConfigFileSnapshot.mockResolvedValue({ ...baseSnapshot });
+
+ await agentsAddCommand(
+ { name: "Work", nonInteractive: true },
+ runtime,
+ { hasFlags: false },
+ );
+
+ expect(runtime.error).toHaveBeenCalledWith(
+ expect.stringContaining("--workspace"),
+ );
+ expect(runtime.exit).toHaveBeenCalledWith(1);
+ expect(configMocks.writeConfigFile).not.toHaveBeenCalled();
+ });
});
diff --git a/src/commands/agents.ts b/src/commands/agents.ts
index 0e7b2684a..304c2eed4 100644
--- a/src/commands/agents.ts
+++ b/src/commands/agents.ts
@@ -18,6 +18,7 @@ import {
import type { RuntimeEnv } from "../runtime.js";
import { defaultRuntime } from "../runtime.js";
import { resolveUserPath } from "../utils.js";
+import { normalizeChatProviderId } from "../providers/registry.js";
import { resolveDefaultWhatsAppAccountId } from "../web/accounts.js";
import { createClackPrompter } from "../wizard/clack-prompter.js";
import { WizardCancelledError } from "../wizard/prompts.js";
@@ -29,11 +30,16 @@ import type { AuthChoice, ProviderChoice } from "./onboard-types.js";
type AgentsListOptions = {
json?: boolean;
+ bindings?: boolean;
};
type AgentsAddOptions = {
name?: string;
workspace?: string;
+ model?: string;
+ agentDir?: string;
+ bind?: string[];
+ nonInteractive?: boolean;
json?: boolean;
};
@@ -50,6 +56,7 @@ export type AgentSummary = {
agentDir: string;
model?: string;
bindings: number;
+ bindingDetails?: string[];
isDefault: boolean;
};
@@ -64,6 +71,10 @@ type AgentBinding = {
};
};
+function createQuietRuntime(runtime: RuntimeEnv): RuntimeEnv {
+ return { ...runtime, log: () => {} };
+}
+
function resolveAgentName(cfg: ClawdbotConfig, agentId: string) {
return cfg.routing?.agents?.[agentId]?.name?.trim() || undefined;
}
@@ -270,7 +281,13 @@ function formatSummary(summary: AgentSummary) {
summary.model ? `model: ${summary.model}` : null,
`bindings: ${summary.bindings}`,
].filter(Boolean);
- return `- ${parts.join(" | ")}`;
+ const lines = [`- ${parts.join(" | ")}`];
+ if (summary.bindingDetails?.length) {
+ for (const binding of summary.bindingDetails) {
+ lines.push(` - ${binding}`);
+ }
+ }
+ return lines.join("\n");
}
async function requireValidConfig(
@@ -300,6 +317,22 @@ export async function agentsListCommand(
if (!cfg) return;
const summaries = buildAgentSummaries(cfg);
+ if (opts.bindings) {
+ const bindingMap = new Map();
+ for (const binding of cfg.routing?.bindings ?? []) {
+ const agentId = normalizeAgentId(binding.agentId);
+ const list = bindingMap.get(agentId) ?? [];
+ list.push(describeBinding(binding as AgentBinding));
+ bindingMap.set(agentId, list);
+ }
+ for (const summary of summaries) {
+ const details = bindingMap.get(summary.id);
+ if (details && details.length > 0) {
+ summary.bindingDetails = details;
+ }
+ }
+ }
+
if (opts.json) {
runtime.log(JSON.stringify(summaries, null, 2));
return;
@@ -340,6 +373,40 @@ function buildProviderBindings(params: {
return bindings;
}
+function parseBindingSpecs(params: {
+ agentId: string;
+ specs?: string[];
+ config: ClawdbotConfig;
+}): { bindings: AgentBinding[]; errors: string[] } {
+ const bindings: AgentBinding[] = [];
+ const errors: string[] = [];
+ const specs = params.specs ?? [];
+ const agentId = normalizeAgentId(params.agentId);
+ for (const raw of specs) {
+ const trimmed = raw?.trim();
+ if (!trimmed) continue;
+ const [providerRaw, accountRaw] = trimmed.split(":", 2);
+ const provider = normalizeChatProviderId(providerRaw);
+ if (!provider) {
+ errors.push(`Unknown provider "${providerRaw}".`);
+ continue;
+ }
+ let accountId = accountRaw?.trim();
+ if (accountRaw !== undefined && !accountId) {
+ errors.push(`Invalid binding "${trimmed}" (empty account id).`);
+ continue;
+ }
+ if (!accountId && provider === "whatsapp") {
+ accountId = resolveDefaultWhatsAppAccountId(params.config);
+ if (!accountId) accountId = DEFAULT_ACCOUNT_ID;
+ }
+ const match: AgentBinding["match"] = { provider };
+ if (accountId) match.accountId = accountId;
+ bindings.push({ agentId, match });
+ }
+ return { bindings, errors };
+}
+
export async function agentsAddCommand(
opts: AgentsAddOptions,
runtime: RuntimeEnv = defaultRuntime,
@@ -351,8 +418,9 @@ export async function agentsAddCommand(
const workspaceFlag = opts.workspace?.trim();
const nameInput = opts.name?.trim();
const hasFlags = params?.hasFlags === true;
+ const nonInteractive = Boolean(opts.nonInteractive || hasFlags);
- if (hasFlags && !workspaceFlag) {
+ if (nonInteractive && !workspaceFlag) {
runtime.error(
"Non-interactive mode requires --workspace. Re-run without flags to use the wizard.",
);
@@ -360,9 +428,9 @@ export async function agentsAddCommand(
return;
}
- if (workspaceFlag) {
+ if (nonInteractive) {
if (!nameInput) {
- runtime.error("Agent name is required when --workspace is provided.");
+ runtime.error("Agent name is required in non-interactive mode.");
runtime.exit(1);
return;
}
@@ -382,18 +450,38 @@ export async function agentsAddCommand(
}
const workspaceDir = resolveUserPath(workspaceFlag);
- const agentDir = resolveAgentDir(cfg, agentId);
+ const agentDir = opts.agentDir?.trim()
+ ? resolveUserPath(opts.agentDir.trim())
+ : resolveAgentDir(cfg, agentId);
+ const model = opts.model?.trim();
const nextConfig = applyAgentConfig(cfg, {
agentId,
name: nameInput,
workspace: workspaceDir,
agentDir,
+ ...(model ? { model } : {}),
});
- await writeConfigFile(nextConfig);
- runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
- await ensureWorkspaceAndSessions(workspaceDir, runtime, {
- skipBootstrap: Boolean(nextConfig.agent?.skipBootstrap),
+ const bindingParse = parseBindingSpecs({
+ agentId,
+ specs: opts.bind,
+ config: nextConfig,
+ });
+ if (bindingParse.errors.length > 0) {
+ runtime.error(bindingParse.errors.join("\n"));
+ runtime.exit(1);
+ return;
+ }
+ const bindingResult =
+ bindingParse.bindings.length > 0
+ ? applyAgentBindings(nextConfig, bindingParse.bindings)
+ : { config: nextConfig, added: [], skipped: [], conflicts: [] };
+
+ await writeConfigFile(bindingResult.config);
+ if (!opts.json) runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
+ const quietRuntime = opts.json ? createQuietRuntime(runtime) : runtime;
+ await ensureWorkspaceAndSessions(workspaceDir, quietRuntime, {
+ skipBootstrap: Boolean(bindingResult.config.agent?.skipBootstrap),
agentId,
});
@@ -402,6 +490,15 @@ export async function agentsAddCommand(
name: nameInput,
workspace: workspaceDir,
agentDir,
+ model,
+ bindings: {
+ added: bindingResult.added.map(describeBinding),
+ skipped: bindingResult.skipped.map(describeBinding),
+ conflicts: bindingResult.conflicts.map(
+ (conflict) =>
+ `${describeBinding(conflict.binding)} (agent=${conflict.existingAgentId})`,
+ ),
+ },
};
if (opts.json) {
runtime.log(JSON.stringify(payload, null, 2));
@@ -409,6 +506,18 @@ export async function agentsAddCommand(
runtime.log(`Agent: ${agentId}`);
runtime.log(`Workspace: ${workspaceDir}`);
runtime.log(`Agent dir: ${agentDir}`);
+ if (model) runtime.log(`Model: ${model}`);
+ if (bindingResult.conflicts.length > 0) {
+ runtime.error(
+ [
+ "Skipped bindings already claimed by another agent:",
+ ...bindingResult.conflicts.map(
+ (conflict) =>
+ `- ${describeBinding(conflict.binding)} (agent=${conflict.existingAgentId})`,
+ ),
+ ].join("\n"),
+ );
+ }
}
return;
}
@@ -633,11 +742,12 @@ export async function agentsDeleteCommand(
const result = pruneAgentConfig(cfg, agentId);
await writeConfigFile(result.config);
- runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
+ if (!opts.json) runtime.log(`Updated ${CONFIG_PATH_CLAWDBOT}`);
- await moveToTrash(workspaceDir, runtime);
- await moveToTrash(agentDir, runtime);
- await moveToTrash(sessionsDir, runtime);
+ const quietRuntime = opts.json ? createQuietRuntime(runtime) : runtime;
+ await moveToTrash(workspaceDir, quietRuntime);
+ await moveToTrash(agentDir, quietRuntime);
+ await moveToTrash(sessionsDir, quietRuntime);
if (opts.json) {
runtime.log(