Scope
Channel-scope is the per-channel opt-in compatibility layer between ClawCode and messaging plugins that publish their own access governance (today: claude-whatsapp's historyScope). When a channel is armed (mode != off, governance resolvable), ClawCode's memory surfaces honor the upstream plugin's access rules; when a channel is mode: off (the default), behavior is identical to having no scope layer.
This is an OPTIONAL feature — see docs/channel-scope-compat.md. Enforcement covers memory_search, memory_get, memory_context, the QMD path, voice_transcribe, dream promote routing, and chat_inbox_read. Per-chat semantics flow through (a) a synthetic per-chat indexer over the upstream messages.db which produces per-chat chunks, and (b) a cross-plugin request envelope contract — claude-whatsapp embeds a requestEnvelopeToken in each inbound notification, and ClawCode resolves the token to a chat/sender binding for the current MCP call. Owner unlock (declarative identity = "owner" + out-of-band trust file) remains the always-available escape hatch; without an envelope (or with an invalid/expired one) under mode = enforce, calls fall through to guest [].
Talk to the user in the language they've been using on this turn — never default to a hard-coded language.
When to use
- After installing claude-whatsapp + pairing it:
/agent:scope statusconfirms the adapter seesaccess.json. - To turn enforcement on:
/agent:scope wizardwalks through the choices, or use the one-liner/agent:scope enable whatsapp shadow(orenforce). - To turn it off:
/agent:scope disable whatsapp. - To inspect existing channel-derived content in shared memory:
/agent:scope audit.
Steps
Step 1 — Parse the subcommand
The first word in $ARGS selects the path:
status→ show every configured channel's runtime stateenable <channel> [shadow|enforce]→ setconfig.scope.<channel>.modedisable <channel>→ set mode tooffwizard→ interactive REPL flow viaAskUserQuestiontest <chatId>→ dry-run probe against the adapter (see Step 6)audit→ re-runmcp__clawcode__agent_doctorand surface only thescope-*rows
If $ARGS is empty or the subcommand is unknown, default to status.
Step 2 — status
- Call
mcp__clawcode__agent_config(action='get')and parse thescopeblock. - For each channel under
scope, display:mode(off / shadow / enforce)identity(auto / owner / guest)background.identity(deny / system-owner)execGate.mode(off / shadow / enforce),execGate.policy(denylist / allowlist), andexecGate.toolscount (or "defaults" when omitted)- WhatsApp only:
accessJsonPath,cwdExactMatchOnly
- Surface trust file presence (read scope + exec):
<channel>-ownerexists? (yes/no) — gates read scope owner unlock<channel>-execexists? (yes/no) — gates execGate "trust this machine" path
- Then call
mcp__clawcode__agent_doctor(action='check')and surface the scope rows (scope-pre-enforce-audit,scope-bypasses,scope-quarantine-pending,scope-execgate-status,scope-execgate-shadow-events).
If scope is absent in config, say so explicitly and recommend /agent:scope wizard.
Step 3 — enable <channel> [mode]
All scope-tree writes are refused by mcp__clawcode__agent_config(action='set') (any key starting with scope). The agent cannot silently elevate or relax the policy; every scope key goes through Bash, which surfaces a permission prompt to the user.
For enable <channel> <mode> the single Bash call covers ALL scope.<channel> keys. Substitute BOTH <channel> (validated against the shipped enum {whatsapp, telegram, discord, imessage, webchat}) AND <mode> ('shadow' or 'enforce'; default 'shadow' when omitted) from $ARGS. Refuse any other literal for either parameter — only those values are valid for enable. cwdExactMatchOnly preserves the prior value if the user had it true:
Bash('node -e "const fs=require(\"fs\"),p=\"agent-config.json\";const c=fs.existsSync(p)?JSON.parse(fs.readFileSync(p,\"utf-8\")):{};c.scope=c.scope||{};const ch=\"<channel>\";const cur=c.scope[ch]||{};c.scope[ch]=Object.assign({},cur,{mode:\"<mode>\",identity:\"auto\",accessJsonPath:cur.accessJsonPath||\"auto\",cwdExactMatchOnly:cur.cwdExactMatchOnly===true,background:Object.assign({},cur.background,{identity:\"deny\"})});fs.writeFileSync(p,JSON.stringify(c,null,2));console.log(\"wrote\",p);"')
The Bash call surfaces a permission prompt to the user — that's intentional. Default mode when omitted: shadow. Confirm by re-running status.
Tell the user: "Run /mcp reconnect clawcode for changes to take effect."
Step 4 — disable <channel>
First validate <channel> is one of the shipped channel names (whatsapp, telegram, discord, imessage, webchat). Reject any other value with an error message — this defends against future channel IDs that might contain shell metacharacters. Currently shipped names are alphanumeric and safe.
scope.<channel>.mode AND scope.<channel>.execGate.mode are both on the security-sensitive blocklist, so this also goes through Bash. A disable resets BOTH read scope AND execGate to off (the user expected "turn the channel off" — they don't expect read scope to flip but exec scope to remain active):
Bash('node -e "const fs=require(\"fs\"),p=\"agent-config.json\";const c=fs.existsSync(p)?JSON.parse(fs.readFileSync(p,\"utf-8\")):{};c.scope=c.scope||{};const cur=c.scope.<channel>||{};c.scope.<channel>=Object.assign({},cur,{mode:\"off\",execGate:{mode:\"off\"}});fs.writeFileSync(p,JSON.stringify(c,null,2));console.log(\"disabled\");"')
Also remove BOTH trust files (read-scope owner trust AND exec trust) for THIS workspace. As of 1.7.0 the trust files live under a per-workspace fingerprint subdir; the fingerprint is computed by the bridge script scripts/print-workspace-fingerprint.mjs so Bash and TS agree byte-exact:
Bash('set -euo pipefail; [ -n "${CLAUDE_PLUGIN_ROOT:-}" ] || { echo "CLAUDE_PLUGIN_ROOT unset"; exit 1; }; TS_HASH=$(node "$CLAUDE_PLUGIN_ROOT/node_modules/tsx/dist/cli.mjs" "$CLAUDE_PLUGIN_ROOT/scripts/print-workspace-fingerprint.mjs" "$PWD"); [[ "$TS_HASH" =~ ^[0-9a-f]{32}$ ]] || { echo "bad TS_HASH=$TS_HASH"; exit 1; }; rm -f -- "$HOME/.claude/agent/scope-trust/$TS_HASH/<channel>-owner" "$HOME/.claude/agent/scope-trust/$TS_HASH/<channel>-exec"')
so both unlocks are dropped along with the mode flip. Both Bash calls surface a permission prompt — intentional: turning scope off and dropping trust are user-visible state changes. Cross-workspace isolation: this only removes THIS workspace's trust; other workspaces are untouched.
Step 5 — wizard (interactive)
Load AskUserQuestion via ToolSearch(query='select:AskUserQuestion') first.
Then walk the user through the choices, ONE question per AskUserQuestion call. Sample flow (substitute the user's language):
-
Which channel?
- "WhatsApp (claude-whatsapp paired)" →
whatsapp - "Cancel" → exit, no changes
(Skip channels that are not installed/paired — only offer those whose
agent_doctorreports installed+authenticated.)
- "WhatsApp (claude-whatsapp paired)" →
-
Mode?
- "Shadow — collect stats but don't filter (recommended for first week)"
- "Enforce — filter denied chunks immediately"
- "Cancel" → exit
-
¿Sos el owner de este WhatsApp? (owner-unlock primitive — two-factor:
identity = "owner"config + out-of-band trust file)- "Sí, soy el owner — quiero que ClawCode pueda buscar en mis chats sin restricción" →
identity: ownerAND step 6 will create a per-workspace trust file (as of 1.7.0) under~/.claude/agent/scope-trust/<workspace-fingerprint>/whatsapp-ownervia Bash. Both writes are required: config alone (which the agent can write) does NOT unlock — the trust file (a separateBashcall the user approves) is what makes the unlock real. The trust is scoped to T
- "Sí, soy el owner — quiero que ClawCode pueda buscar en mis chats sin restricción" →