OpenClaw channel plugin for Basecamp. Connects Campfire chats, card tables, to-do lists, check-ins, pings, and message boards to OpenClaw agents -- every Basecamp surface as a live agent interaction point.
Plugin id: basecamp (the npm package name is @37signals/openclaw-basecamp; the plugin id is what plugins.allow, plugins.entries.*, openclaw plugins inspect, and tools.allow use).
- OpenClaw >= 2026.8.1 (
openclaw.compat.pluginApiandpeerDependencies.openclawboth enforce this at install and load) - Node.js
>=22.22.3 <23 || >=24.15.0 <25 || >=25.9.0(the OpenClaw host range;engines.node) - A Basecamp account plus either:
- An OAuth app you control (client ID + secret), registered at launchpad.37signals.com/integrations, or
- An authenticated Basecamp CLI profile with stored credentials that the onboarding wizard can import
openclaw plugins install npm:@37signals/openclaw-basecamp --force --accept-capabilities-
npm:selects the npm registry deterministically. The package is not in OpenClaw's bundled/official catalog, so a non-interactive install must pass--forceto confirm the source (interactive installs prompt instead).--forcedoes not bypasssecurity.installPolicy. -
--accept-capabilitiesrecords consent for the capability surface the manifest declares: thebasecampchannel and the tenbasecamp_*tools. Interactive installs show the review screen instead. A later update that widens that surface asks for consent again (openclaw plugins update basecamp --accept-capabilities). -
OpenClaw logs
Plugin manifest id "basecamp" differs from npm package name "@37signals/openclaw-basecamp"; using manifest id as the config key.This is informational and expected. -
If your config uses a
plugins.allowlist, add the plugin id, not the package name:plugins: allow: ["basecamp"]
The plugin activates lazily (activation.onChannels: ["basecamp"]): it loads once channels.basecamp is configured. While the Gateway is still starting its plugins, the webhook path answers 503 Retry-After: 1; Basecamp retries, and webhooks are accelerators only.
Verify the install without starting the Gateway:
openclaw plugins inspect basecamp --runtime --jsonopenclaw channels add basecampThe onboarding wizard handles OAuth app setup (or import from a Basecamp CLI profile), browser-based login, identity discovery, Basecamp account selection, and writing the channel config. The same fields are available as flags for non-interactive setup -- openclaw channels add basecamp --help lists only Basecamp's options:
| Flag | Config key | Notes |
|---|---|---|
--token <token> |
accounts.<id>.token |
Inline OAuth/bearer token (prefer --token-file or a SecretRef) |
--token-file <path> |
accounts.<id>.tokenFile |
File containing the token |
--person-id <id> |
accounts.<id>.personId |
Numeric Basecamp person ID of the service account |
--basecamp-account-id <id> |
accounts.<id>.basecampAccountId |
Numeric Basecamp account ID for API calls |
--oauth-client-id <id> |
accounts.<id>.oauthClientId |
Basecamp OAuth app client ID |
--oauth-client-secret <secret> |
accounts.<id>.oauthClientSecret |
Basecamp OAuth app client secret |
--cli-profile <name> |
accounts.<id>.cliProfile |
Basecamp CLI profile used only for setup identity discovery |
Re-authenticate an OAuth account later with openclaw channels login --channel basecamp [--account <id>]. openclaw channels status --probe reports each account's lifecycle (starting / ready / recovering while a poller circuit breaker is open / blocked on auth failure / stopped), whether webhook ingress is unavailable, and the last poller activity.
All configuration lives under channels.basecamp. The schema is defined in code (src/config.ts) and projected into the manifest's channelConfigs by npm run manifest:gen, so the Control UI and openclaw doctor validate it without loading the runtime.
| Key | Type | Description |
|---|---|---|
enabled |
boolean |
Enable or disable the channel |
accounts |
object |
Per-account auth and identity (personId, token/tokenFile, basecampAccountId, OAuth overrides) plus the standard per-account policy leaves (dmPolicy, allowFrom, groupPolicy, groupAllowFrom, mentionPatterns, historyLimit, markdown.tables, ...). Account-level values override the channel-level ones below. |
personas |
object |
Map agent IDs to Basecamp account IDs; the agent replies and runs tools as that account |
virtualAccounts |
object |
Synthetic account IDs scoped to one project: { accountId, bucketId } |
dmPolicy |
"pairing" | "allowlist" | "open" | "disabled" |
How Pings (DMs) from unknown people are handled. Default: "pairing" — the requester gets a pairing code by Ping and openclaw pairing approve admits them; "allowlist" admits only allowFrom. |
allowFrom |
string[] |
Basecamp person IDs permitted to DM agents ("*" with dmPolicy: "open") |
groupPolicy |
"open" | "allowlist" | "disabled" |
Sender policy for project surfaces (Campfires, comments). Default: "open" |
engage |
string[] |
Engagement types that reach an agent: dm, mention, assignment, checkin, conversation, activity |
buckets |
object |
Per-project overrides keyed by bucket ID (or "*"): requireMention, tools.allow/tools.deny, engage, allowFrom, enabled |
webhooks |
object |
Webhook subscription config: payloadUrl, projects, types, autoRegister, deactivateOnStop |
webhookSecret |
string | SecretRef |
Token for webhook URL verification (?token=). Required for the webhook route to accept requests |
oauth |
object |
Channel-level OAuth app credentials (clientId, clientSecret) shared across accounts |
polling |
object |
Polling intervals: activityIntervalMs (default 120s), readingsIntervalMs (60s), assignmentsIntervalMs (300s) |
retry |
object |
Retry behavior: maxAttempts, baseDelayMs, maxDelayMs, jitter |
circuitBreaker |
object |
Circuit breaker: threshold, cooldownMs |
safetyNet |
object |
Safety net polling for missed events: projects, intervalMs |
reconciliation |
object |
Gap reconciliation: enabled, intervalMs, gapThreshold |
token, oauthClientSecret, webhookSecret, and oauth.clientSecret accept either a literal string or a SecretRef (source: "env" | "file" | "exec" | "store") resolved by the OpenClaw secrets runtime, so plaintext never has to live in the config file:
channels:
basecamp:
webhookSecret: { source: "env", provider: "default", id: "BASECAMP_WEBHOOK_SECRET" }
accounts:
main:
personId: "12345678"
basecampAccountId: "9999999"
token: { source: "env", provider: "default", id: "BASECAMP_TOKEN" }tokenFile remains as shorthand for a file SecretRef. openclaw secrets audit --check reports which Basecamp fields are still plaintext.
Events that reach an agent because of engage: ["conversation", "activity"] -- unmentioned Campfire lines, comments, card moves -- are classified as room events rather than user turns by default: the agent sees them as quiet context and speaks only by calling the message tool. Set messages.groupChat.unmentionedInbound: user_request (globally, or per agent under groupChat) to make every engaged event a full turn again; visibleReplies: message_tool keeps ambient replies on the tool path:
messages:
groupChat:
unmentionedInbound: room_event # Basecamp default; user_request opts out
visibleReplies: message_toolMentions, DMs, assignments, and check-in prompts stay full user turns. requireMention: true on a bucket drops unmentioned traffic before it becomes a room event.
When several personas share a Campfire, the channel attaches bot-loop facts to any turn whose sender is another configured persona. Set the shared baseline once:
channels:
defaults:
botLoopProtection:
maxEventsPerWindow: 20
windowSeconds: 60
cooldownSeconds: 60Outbound targets use the same peer grammar as inbound sessions, optionally prefixed with basecamp: or bc::
| Target | Posts |
|---|---|
recording:<id> |
A Campfire line or comment on a recording the channel has already seen (resolved through the recording→bucket index) |
bucket:<id> |
A Campfire line in the project's default chat |
bucket:<b>/recording:<r> |
A comment on a recording the channel has never seen (cold send with an explicit bucket) |
ping:<id> |
A line in a Circle (Ping) chat; the id is the circle's bucket id |
A bare number is treated as recording:<id>. A recording: target that is not in the index fails with an error naming the explicit bucket:<b>/recording:<r> form.
openclaw message send --channel basecamp --target recording:11111 --message "Deploy finished."The same resolution serves cron and heartbeat announcements, sessions_spawn, the shared message tool, and the delivery paths below. Media is posted as a link on chat targets and as an attachment on commentable recordings; only text is declared durable-final.
- Approvals (exec, plugin, recurring work) arrive as a comment or Campfire line describing the pending operation. Basecamp has no buttons, so approve in the same chat with the core text command:
/approve <id> allow-once,/approve <id> allow-always, or/approve <id> deny. Slash commands on this channel are owner-only (enforceOwnerForCommands), so list approvers' Basecamp person IDs incommands.ownerAllowFrom. - Structured questions (
ask_user) render as a numbered option list; reply with a number, an option label, or free text in the same recording. - Private credential requests (the
secretstool) never accept a value in chat. OpenClaw delivers a Control UI link, which needsgateway.publicOriginset to the externally reachable Gateway origin; without it the agent reports a visible blocker instead of a dead link. - Rich presentations (
--presentation, tool payloads) render titles, context, dividers, and tables as Basecamp HTML; buttons, selects, and charts degrade to deterministic text.
Agents connected through this channel have access to the following tools:
| Tool | Description |
|---|---|
basecamp_create_todo |
Create a new to-do item in a Basecamp to-do list |
basecamp_complete_todo |
Mark a to-do as complete |
basecamp_reopen_todo |
Reopen a completed to-do (mark as incomplete) |
basecamp_read_history |
Fetch recent messages or comments from a recording (chat transcript or comments) |
basecamp_add_boost |
Add a boost (reaction) to any recording -- emoji or short celebratory text |
basecamp_move_card |
Move a card to a different column in a card table |
basecamp_post_message |
Post a new message to a message board |
basecamp_answer_checkin |
Answer a check-in question |
basecamp_api_read |
GET any Basecamp 3 API resource (projects, people, todos, documents, schedules, etc.) |
basecamp_api_write |
POST/PUT/DELETE any Basecamp 3 API resource |
Tools are registered through api.registerTool and listed in the manifest's contracts.tools, so they appear on the capability-consent screen and can be allowlisted per agent with tools.allow: ["basecamp"] (every tool from the plugin) or by name. A tool call runs under the account mapped for the calling agent in channels.basecamp.personas, falling back to the default account. Without any configured account the tools still register and return { ok: false, error } naming the missing config instead of silently disappearing.
openclaw message send --channel basecamp …is rejected at CLI argument validation withUnknown channel "basecamp". The CLI checks the requested channel against the process-root plugin registry, which a CLI process never activates for non-bundled plugins (the plugin is loaded into a request-scoped registry instead). Sends through the gateway — the agent'smessagetool, cron/heartbeat announcements, and the channel'soutboundadapter — are unaffected.- Durable ingress queues and keyed/blob stores are trust-gated to bundled and official-catalog plugins; see
docs/upgrades/official-catalog-nomination.md. - Basecamp's account-wide progress feed omits the authenticated user's own activity, so a single-person sandbox cannot drive the poller with its own posts; use webhooks (or a second person) when testing inbound delivery.
OpenClaw's keyed stores and durable ingress queues are reserved for bundled and official-catalog plugins, so this plugin keeps its own state as JSON under <stateDir>/plugins/basecamp/:
cursors-<account>.json-- polling cursorsrecording-index-<account>.json-- recording→bucket index for outbound targets and project-level bindingswebhook-secrets-<account>.json-- per-project webhook HMAC secretstokens/<account>.json-- OAuth tokens (with a companion.client.json)
The directory is declared in the manifest's backupResources, so openclaw backup includes it. Replay-guard (exactly-once) state lives in OpenClaw's shared plugin-state SQLite store with a 24h TTL and needs no separate backup. Webhook delivery is at-most-once, bounded by a per-account in-flight limit; correctness comes from polling.
git clone <repo-url>
cd openclaw-basecamp
npm install
npm run build # required — OpenClaw loads dist/index.js and dist/setup-entry.js
npm run check # typecheck + lint + tests
npm run manifest:gen # regenerate openclaw.plugin.json after schema or tool changes
bash scripts/verify-pack.shRun the checkout inside a local Gateway either as a managed link install (creates an install record; re-consents on every --force reinstall):
openclaw plugins install --link /path/to/openclaw-basecamp --force --accept-capabilitiesor as a bare load path (no install record, no consent tracking):
plugins:
load:
paths: ["/path/to/openclaw-basecamp"]Either way, rebuild (npm run build) after each change; the Gateway loads dist/. CI additionally proves the packed artifact installs cleanly: npm pack, then openclaw plugins install npm-pack:<tgz> --force --accept-capabilities and openclaw plugins inspect basecamp --runtime --json in a scratch OPENCLAW_STATE_DIR.