Skip to main content

Operations

Running marsClaw as a long-lived service: launchd integration, backups, observability, and the troubleshooting cheatsheet.

Run as a launchd service (macOS)

Foreground bun run start is fine while you tinker. For "always on", install the user-level launchd agent:

bun run service install # render plist with resolved paths, copy to ~/Library/LaunchAgents, bootstrap
bun run service status # loaded? log paths? binary still in place?
bun run service start
bun run service restart # kickstart -k → SIGTERM, respawns into current code (use after pulling)
bun run service stop
bun run service uninstall
bun run service logs # tail logs/marsclaw.log

The agent runs as your user (no root), with KeepAlive=true so a crash respawns. Stdout/stderr from launchd itself goes to logs/launchd-stdout.log / logs/launchd-stderr.log as a fallback; the structured log goes to logs/marsclaw.log.

Plist template: launchd/com.marsclaw.plist. Implementation: src/cli/service.ts, src/lib/launchd.ts.

Backups

Daily backups, kept for 7 days by default, written by src/lib/backup.ts:

TargetDestination
data/marsclaw.dbdata/backups/marsclaw-YYYY-MM-DD.db (via VACUUM INTO)
MEMORY.mddata/backups/MEMORY-YYYY-MM-DD.md
data/whatsapp-auth/data/backups/whatsapp-auth-YYYY-MM-DD.tar.gz
bun run backup # one-shot — does the same thing

Override the schedule via env:

KeyDefault
MARSCLAW_BACKUP_DIRdata/backups
MARSCLAW_BACKUP_KEEP7 (days)

Observability

Status snapshot

bun run status

Shows provider, DB stats (message count per thread, last-active timestamp), and recent activity. Implemented in src/cli/status.ts.

Usage / spend

bun run usage today
bun run usage week
bun run usage by-thread

Anthropic-only — sums total_cost_usd from successful turns. Skipped under Claude Pro/Max OAuth.

DB maintenance

bun run db stats # row counts per table, file size, last vacuum
bun run db integrity # PRAGMA integrity_check
bun run db vacuum # reclaim space; brief write lock

Logs

logs/marsclaw.log is the main log. Rotation: src/lib/log-rotate.ts keeps it bounded.

tail -f logs/marsclaw.log
LOG_LEVEL=debug bun run start # noisier

Levels: debug / info / warn / error / fatal.

Health endpoint

src/lib/health-server.ts exposes a small HTTP server (default port behaviour: see the source) returning JSON with channel readiness and DB row counts. Useful as a liveness probe for an external watchdog.

Heartbeat file

A provider in mid-turn touches data/heartbeat. External tooling can mtime-check this to detect stuck turns.

Audit log

Every tool decision (allow, deny, mutation-blocked) lands as one JSON line in logs/audit.log — separate from the app log so it can be reasoned about / shipped on its own. Override the path with MARSCLAW_AUDIT_LOG. Each record carries ts, pid, tool, decision, layer (which gate decided), an optional reason, and a redacted subject (URL, file path, command preview). See security.md for the schema and inspection recipes.

grep '"decision":"deny"' logs/audit.log | tail
grep '"layer":"mutation-gate"' logs/audit.log
grep '"tool":"WebFetch"' logs/audit.log

It's a local, append-only forensic trail — not tamper-resistant against host compromise. Ship to a remote sink (syslog or similar) if you need that.

Updating

bun run update # git pull, bun install, service restart
bun run update --force # blow past local changes (use with care)

Source: src/cli/update.ts.

Smoke test

bun run smoke
bun run smoke "what is 2+2"

Fires a synthetic message all the way through handleMessage. Doesn't go through a channel — useful for verifying provider auth and tool wiring without messaging yourself. Source: src/cli/smoke.ts.

Hardening defaults you should know about

For the full threat model, capability flags, and residual risks, see security.md. Operationally:

MechanismWhereEffect
Capability flags (off by default)allow_shell, allow_web, allow_mutating_tools in data/config.jsonNo shell, no WebFetch/WebSearch, no outbound/mutating Google calls until you opt in. Out-of-the-box the agent has no third-party egress path.
Web egress allow-listallowed_web_domains + src/lib/url-allowlist.tsWhen allow_web is on, WebFetch is gated to approved hosts. Look-alike domains and non-http(s) URLs are rejected.
Sender allow-lists (per channel)allowed_jids / allowed_telegram_chats / allowed_slack_usersDrops inbound messages from anyone not listed before they reach the agent.
Sensitive-path guardsrc/lib/sensitive-paths.ts.env, data/secrets, data/config.json, data/whatsapp-auth, data/marsclaw.db, ~/.claude.json, ~/.gemini blocked from FS tools and send_file regardless of allowed_paths.
Grep/Glob recursion gatesrc/lib/tool-permissions.tsRecursive tools refuse a search root that contains any sensitive subtree; bare Grep/Glob no longer silently scans cwd.
Mutation gatesrc/lib/mutation-gate.tsgmail_send, sheets_write, calendar_create_event, write-style *_raw calls refuse to run unless allow_mutating_tools is set.
Researcher subagentagents.researcher in src/providers/claude-sdk.tsWeb reads run in an empty-room context (tools: ['WebFetch'], no FS / MCP / history) with persona-level "treat fetched content as untrusted" framing.
Audit logsrc/lib/audit-log.tsAppend-only JSON-lines record of every tool decision at logs/audit.log.
MCP child env passthroughMCP_ENV_PASSTHROUGH in src/providers/claude-sdk.tsThe MCP server (the broker) does not receive ANTHROPIC_API_KEY — least-privilege env.
Per-thread serializationsrc/index.ts inFlight mapTwo messages in same chat never run two agent calls in parallel.
Startup circuit breakersrc/lib/circuit-breaker.tsSleeps progressively before boot if recent restarts have been suspiciously frequent — prevents crash loops from burning API quota.
Inbound rate-limitsrc/lib/rate-limit.tsPer-sender token bucket (rate_limit_per_minute, rate_limit_per_hour).
Cost capsrc/lib/cost-tracker.tsRefuses new turns once today's spend exceeds daily_usd_budget (metered Anthropic only).
Outbox attempt capsrc/db/outbox.tsPermanently fails delivery after MAX_ATTEMPTS retries; visible in logs.
Attachment safetysrc/lib/attachment-safety.tsValidates inbound media size and mime.
Backupssrc/lib/backup.tsDaily DB + memory + WhatsApp auth snapshot.

Troubleshooting

SymptomLikely causeFix
WhatsApp cycling code=405/428Outdated Baileys protocolbun update baileys
Connected but no repliesWhatsApp replaying history (type: append)Wait, then send a fresh message
[claude] timeout after 300000msLong tool loop / networkBump MARSCLAW_AGENT_TIMEOUT_MS
[gemini] quota exhaustedOAuth free tier hitSwitch with bun run provider claude, or set GEMINI_API_KEY
Reply duplicated 2-3×Outbox drain-race (fixed in current src/index.ts)Pull latest, restart
[whatsapp] skipped non-text (audioMessage)Voice disabledbun run voice start + MARSCLAW_VOICE=1
transcribe failedWhisper sidecar downbun run voice status
kokoro sidecar error in speakKokoro sidecar downbun run voice start
Outside allowed_pathsAgent tried to touch a path you didn't allowlistbun run path add <dir>
holds secrets or marsClaw's own permission configAgent tried to read .env, data/secrets, or similarBy design — secrets aren't reachable. See security.md.
Search root … contains sensitive filesGrep/Glob was pointed at a root that straddles .env etc.Narrow the search to a subdir (e.g. src/)
Shell/Bash is disabledAgent tried to run a shell commandSet allow_shell: true in data/config.json only if you accept the exfil-risk trade-off
Fetch denied: host … not on the allowlistWebFetch URL's host isn't in allowed_web_domainsAdd the domain to data/config.json and restart; see security.md
WebFetch is disabled / WebSearch is disabledallow_web is falseSet allow_web: true and populate allowed_web_domains
Refused: "gmail_send" … disabled by defaultMutating MCP tool blockedSet allow_mutating_tools: true if you really want the bot to act outwardly
Telegram / Slack messages ignoredSender not on allowed_telegram_chats / allowed_slack_usersCopy the chat / user id from the warn log into data/config.json and restart
daily budget exceededAnthropic spend cap hitWait for midnight UTC reset, or bump daily_usd_budget
[whatsapp] giving up after 5 failed connection attemptsToo many linked devices, or geo blockUnlink, try another network
Service won't loadPlist references stale bun pathbun run service install to re-render