Skip to content

Security

RingClaw is a remote-control bridge from RingCentral Team Messaging into a host machine that runs AI agents with broad filesystem and shell access. The threat model and defense layers below describe how the project keeps that power scoped to the right people through config, command authorization, OOB approvals, and process-local state.

Quick navigation

If you want to …Read
Decide who can drive the bot at allSender Allowlist
See which slash commands each user may runCommand Authorization
Understand the cross-chat ACTION gateCross-Chat Actions
Use full_access or /full-access grantACP Full-Access
Approve an OOB challenge from the hostApproval CLI
Pin the agent's working directoryWorkspace Allowlist
Lock down the local HTTP API or compare Bot vs Private AppAPI & Clients

Threat model

The trust assumption underpinning every defense layer is that an attacker with a foothold inside RingCentral — account compromise, prompt injection in the bot DM, or a malicious teammate impersonating the owner — does not simultaneously have shell access to the host running RingClaw.

That single assumption is what makes the OOB approval design defensible:

  • All approvals (/full-access grant, non-owner cross-chat ACTIONs, authorize-mention) require running ringclaw approval <id> on the host.
  • A compromised RC account can read the challenge ID from the owner DM but cannot execute the approval without SSH or physical access to the box.
  • All OOB state is in-memory and cleared on restart, so a crash-restart naturally re-locks the bot until the operator explicitly re-grants.

If the host machine is compromised at the OS level, RingClaw cannot help — the API token, the config file, and any agent secrets are already accessible. The relevant question is whether RingClaw adds new attack surface beyond what an attacker would already have on a compromised box; the layered model below answers "no" by keeping every privileged surface either config-only or gated on host access.

Four entry points (not one)

RingClaw can act on RingCentral via four distinct entry points. The three-layer permission model in the next section only applies to the WebSocket message path; each of the other entries has its own gate and is listed here so operators do not mistake "non-owner cannot use /cwd in a group" for "non-owner cannot make the bot do anything":

Entry pointLayer 0 (sender)Layer 1 (commands)Layer 2 (ACTION fan-out)Layer 3 (ACP mode)What actually gates it
WebSocket messageYESYESYESYESChat allowlist + sender allowlist + handler checks
HTTP API (/api/send, /api/tasks, /api/notes, /api/events, /api/cards)NONONOn/aAPI token + loopback Host only (api/auth.go); see API & Clients
Cron jobNOjob is created via /cron add (Layer 1); execution has no human senderNO — ACTION blocks are NOT executed; reply is posted verbatimYESJob config in ~/.ringclaw/cron/jobs.json
HeartbeatNOn/a (config-driven)NO — ACTION blocks are NOT executedYESheartbeat.enabled + HEARTBEAT.md

API token equals machine operator

Anyone with read access to ~/.ringclaw/api_token bypasses Layers 0–2 entirely — they can send arbitrary text/media to any chat and create/delete any task, note, event, or card through /api/.... Treat the token file like an SSH key. The default loopback-only bind (api_addr: 127.0.0.1:18011) limits the blast radius to local processes on the same host.

Chat allowlist is the outermost ring

Layer -1: messages from chats not in ringcentral.chat_ids are dropped by the WebSocket monitor before Layer 0 even applies (ringcentral/monitor.go). If a message silently disappears, check the chat allowlist first — the log line reads ignoring message from non-allowed chat.

Permission Matrix

The WebSocket message path is gated through three orthogonal layers that compose bottom-up. A message must clear every applicable layer before it takes effect.

LayerQuestion it answersDetail page
0Can this sender drive the bot at all?Sender Allowlist
1Can this sender run this slash command?Command Authorization
2Can this AI-emitted ACTION fan out (especially cross-chat)?Cross-Chat Actions
3What can the ACP session read, write, or execute?ACP Full-Access

Full access only affects Layer 3

The /full-access grant (and the static full_access: true config) only changes ACP session mode. It does not unlock any chat command — a non-owner still cannot use /cwd in a group, and /full-access itself is still DM-only. It also does not relax the Layer 2 cross-chat fail-closed notice.

DM is the trust boundary, not "owner only"

Layer 1's owner-only gate for privileged commands (/cwd, /cron, /new, /reload, summarize NL triggers) fires in group chats. In bot DMs, the gate applies only when a Private App is configured — in that case privileged commands are restricted to the Private App owner even in DM. Without a Private App, RingClaw has no way to tell "owner" from "another trusted sender in their own DM", so every trusted sender gets full privileged-command power in their own DM with the bot. If you list multiple people in ringcentral.source_user_ids without a Private App, you are trusting all of them equally — including with /cron add, which can keep running arbitrary prompts after the sender walks away.

Layer 0 reminder

Every message first passes through the trusted-sender allowlist, enforced twice (ringcentral/monitor.go on the socket and messaging/handler.go on the handler). Senders outside the allowlist are dropped before any layer below applies.

The Layer 0 set is the union of ringcentral.source_user_ids, the auto-injected Private App owner ID, and the destination chat's ringcentral.chat_user_allow[<chatID>] entry (when present). Authorize-mention approvals land in chat_user_allow — they widen Layer 0 only, never any of the layers below. See Sender Allowlist for the full picture.

Configuration changes worth flagging at upgrade time

Operators upgrading from an old release should confirm the following before restarting:

SettingWhereBehavior to confirm
ringcentral.source_user_idsconfig.jsonEmpty list + Private App = owner-only (auto-injected). Empty list + no Private App = deny all with startup error. See Sender Allowlist.
agent_workspaceconfig.jsonContinues to be the default cwd, AND is implicitly added to the cwd allowlist. See Workspace Allowlist.
agent_allow_workspace_listconfig.jsonExplicit list of directories that /cwd and Agent.SetCwd may target. Always merged with ~/.ringclaw/workspace and (if set) agent_workspace.
agents.<name>.full_accessconfig.json (per ACP agent)true is ignored unless the top-level full_access_ack: true also appears; otherwise downgraded with a WARN log. See ACP Full-Access.
full_access_ackconfig.json (top-level)true honors full_access, false or unset refuses.
ringcentral.allow_group_mention_authorizeconfig.json (under ringcentral)Off by default since v0.4.2 (the v0.4.1 default-on flip was reverted as a security stop-gap). Set to true explicitly to enable. After deny / expire, the same (chat, user) pair is silenced for 24 h. Requires Private App + resolvable owner DM. See Sender Allowlist › SECURITY ADVISORY.
ringcentral.chat_user_allowconfig.json (under ringcentral)Per-chat trusted-sender exception. v0.4.4+: preserved across restarts. Listed users run under the v0.4.3 non-owner ceiling (read-only ACP mode + fail-closed deny of fs/* / terminal/* / session/request_permission). Map keys must be numeric RC chat / group IDs — see Sender Allowlist › How to find a chat ID.
agents.<name>.restricted_mode_idconfig.json (under each agent)v0.4.3+: ACP modeID applied to non-owner sessions. Defaults: droid → spec, claude / gemini / qwen / cursor-agent → plan, others fall back to a heuristic over availableModes (plan, spec, read, safe). When neither path yields a modeID and no override is set, non-owner messages are refused outright. The override must match a mode the agent advertises; otherwise the built-in selection still wins. See Sender Allowlist › SECURITY ADVISORY (v0.4.2 → v0.4.3).

The previously supported RC_* / RINGCLAW_* / OPENCLAW_GATEWAY_* env-var fallbacks have been removed and are silently ignored. All configuration lives in ~/.ringclaw/config.json.

WARNING

After upgrading, operators who relied on the legacy "empty source_user_ids = allow everyone" behavior will see the bot drop every incoming message until they either (a) configure a Private App (the owner is auto-trusted) or (b) populate ringcentral.source_user_ids. The startup log line sender allowlist is empty: ... is the canonical signal for this case.