| description | Connect Claude Code, OpenCode, Cursor, Zed, Devin, Pi, OpenClaw, and MCP-capable assistants to OpenPets through local companion events. |
|---|
OpenPets reacts to coding agents. Most supported agents have an integration package that does two jobs: configure the agent to talk to OpenPets, and at runtime translate the agent's activity into safe pet reactions sent over local IPC or an explicitly configured remote client. Zed is intentionally configuration-only: it runs the MCP server but provides no lifecycle hooks of its own. This doc covers Claude Code, MCP, OpenCode, Cursor, Zed, Devin, Pi, OpenClaw, and DSH, the shared speech-safety layer, and the CLI commands that orchestrate them.
For the local and remote wire protocols, see IPC and remote control. Source maps live
in each packages/*/codemap.md.
Every integration follows the same contract, which is worth internalizing once:
- Configuration is safe and reversible. Each integration uses a transaction
suited to its config format. Most writes use temp-file + rename with a backup
first; Zed settings use a sibling lock, an fsynced temp file, an exclusive
backup, journaled claim and withdrawal artifacts, and no-clobber hard-link
publication.
Paths are validated against traversal/symlink escape; managed entries are
marked so they can be detected, updated, and removed without clobbering the
user's own config. Status is always classified
(
missing/installed/needs-update/conflict/invalid/…), so the UI and CLI can offer the right action. - Runtime is fire-and-forget. Agent events are classified into a reaction and/or a speech category, dispatched non-blocking, and any IPC failure is swallowed. The pet must never slow down or break the agent.
- Speech is always safe. Automatic messages come from validated pools (see below), never from raw prompt/output text.
- Leases route the pet. Integrations acquire a lease on first activity, heartbeat it, and release on shutdown. See the lease model in IPC and remote control.
OpenClaw is the deliberate exception to this shared runtime shape. Its native plugin is local-only, targets the default pet through the local client, does not acquire a lease, and observes hook invocation without reading event arguments.
Remote mode is the explicit exception to the lease rule: it is default-pet-only
and does not acquire, heartbeat, or release leases. It is selected only through
@open-pets/client remote options or the named OPENPETS_REMOTE_ENDPOINT,
OPENPETS_REMOTE_TOKEN, and optional OPENPETS_REMOTE_CLIENT_ID environment
configuration. The client does not read local discovery in that mode. The
remote server exposes only scoped status, default-pet reactions, and optional
short safe messages; it does not accept arbitrary pet IDs, prompt/output text,
paths, files, media, installs, or discovery operations.
Remote v1 is unencrypted raw TCP. Do not use it on public Internet paths, port-forwarded listeners, or shared/untrusted Wi-Fi; prefer an encrypted overlay with its own ACLs. A CGNAT-range endpoint is only an allowed private address classification, not an encryption boundary. Remote control configuration, client pairing, token rotation, and credential revocation can be managed directly in Control Center under Settings → Remote.
When the pet pool is disabled, every agent session that does not pass
--pet <id> shares the single default pet. The pet pool preference (Control
Center → Settings → General, petPoolEnabled, on by default) changes this so
concurrent sessions each get their own pet from a user-configured ordered list.
How it works when enabled:
- The user configures an ordered list of installed pets in Settings. Slot 1 is the primary/default pet; subsequent slots are assigned to additional concurrent sessions in order.
- Pool assignment walks the configured list in order and assigns the first eligible pet without an active lease.
- If no eligible pet in that ordered list is free, the session uses the default pet for its lease. It does not move to a pool pet if a listed slot later becomes free. It does not choose a random installed pet outside the list.
- When a session ends its lease, its pet slot is freed and available to a new session.
--pet <id>always takes priority and bypasses the pool entirely - unchanged from current behavior.
Eligible pool pets are installed, non-broken pets other than the built-in or current default pet, with any pet currently pinned by the user also excluded. Broken or uninstalled pets are skipped silently. Only pets listed in the configured pool order can be assigned; if none of those eligible entries is free, the session uses the default pet for its lease and does not move to a pool pet when a slot later frees up. That freed slot is available to a new lease.
Cross-platform and agent-agnostic. Pool assignment is pure lease logic with
no platform dependency - it works on macOS, Windows, and Linux. Any agent that
acquires a lease through the shared OpenPets client benefits automatically: Claude
Code CLI, opencode, Cursor, and any other MCP client all go through the same
lease.acquire path.
When the pool is disabled, behavior is unchanged: all sessions
without --pet share the single default pet.
packages/agent-events/ is the shared guardrail. It provides curated speech
pools by category - thinking, success, error, permission, question - and the
validators that keep messages safe: single line, 1–140 chars, and rejecting
code, URLs, file paths, and secret-like tokens. pickHookSpeech(category)
selects a message; validateHookSpeech() enforces the rules. claude,
opencode, and pi all depend on it so no integration can leak sensitive text
into a bubble.
The deepest integration, because Claude Code has a rich hook system.
- MCP setup (
claude-code.ts): registers an MCP server namedopenpetsusingclaude mcp add/get/remove. Command modes:published(npx -y @open-pets/mcp),local,bundled(ASAR-unpacked path). Paths are validated to stay within expected directories. Packaged builds default tobundledbut honor an explicitpublishedchoice, and command probing includes fnm default-alias directories plus the standard Windows npm global shim directory, so version-manager installs and globally installed CLIs resolve without shell profile evaluation. The Control Center reads status withclaude mcp get openpets, which only checks the OpenPets entry.claude mcp listhealth-checks every configured server and can outlast the status timeout, so it is only a fallback whengetfails for a reason other than "no such server" (apps/desktop/src/agent-setup-claude.ts). - Hooks (
hook-settings.ts+hooks.ts): installs command hooks into~/.claude/settings.jsonfor the lifecycle eventsUserPromptSubmit,PreToolUse,PermissionRequest,Notification,Stop,StopFailure. Each managed entry carries the--openpets-managedmarker.runClaudeHookFromStdin()maps an event to a reaction: prompt submit → thinking, permission → waiting (aPermissionRequestforAskUserQuestionspeaks "Question for you"-style question speech instead of "Approval needed", since it asks, not approves), stop → success, stop-failure → error, andPreToolUseis classified by tool (Edit/Write/MultiEdit → editing, Bash test commands → testing). - Project-local awareness: if a project defines its own OpenPets hook
(
.claude/settings.local.jsonwith--project-local), the global hook stands down to avoid double-firing. - Throttling: ~20s speech / ~3s permission / ~10s reaction cooldowns via a JSON state file, so the pet doesn't chatter.
- Memory: the desktop's
claude-memory.tsmanages~/.claude/openpets.md(the instructions file telling Claude how to use the pet).
Doctor/install/uninstall helpers (installClaudeHooks, doctorClaudeHooks, …)
are what the Control Center Integrations page and the CLI call.
A standalone stdio MCP server (open-pets-mcp) for any MCP-capable agent. It
registers exactly three tools - openpets_status, openpets_react,
openpets_say - with Zod-validated input and read-only/idempotent annotations.
On startup it acquires a lease, heartbeats every ~5s, and releases on transport
close or SIGINT/SIGTERM. Shutdown marks the shared lease context as closing,
stops timers, waits for in-flight startup and single-flight recovery from either
the heartbeat timer or a reaction/say tool, then best-effort releases every
retained lease ID exactly once before closing the server and exiting. A heartbeat
failure that arrives after closing begins cannot erase the active lease needed
for teardown; a failure just before closing retains its stale ID alongside any
eventual recovery lease. No new recovery acquisition starts after teardown
begins, so an agent that replaces an MCP process cannot leave a temporary second
pool pet behind. Errors are sanitized so IPC paths/tokens/sockets never leak
into tool output. It is spawned by the CLI (runMcp()) which forwards stdio and
signals. --pet <id> targets a specific pet.
Window confinement requires an installed pet. Passing
--pet <id>only activates window confinement when the requested pet is actually installed. If the pet ID is misspelled or not yet installed, the MCP server silently falls back to the default (unconfined) pet. OpenPets now surfaces this via a desktop notification when the fallback occurs. To list installed pets runopenpets pets; to install one useopenpets install <pet-id>or the Pets tab in Control Center.
Ships both a config manager and a runtime plugin.
-
Config (
opencode-config.ts, JSONC-aware): managesmcp,instructions, andpluginarrays in the effective OpenCode config (project.opencode/or global~/.config/opencode/), choosing the right file amongconfig.json/opencode.json/opencode.jsoncand preserving user arrays. Managed instruction blocks use<!-- OPENPETS:START/END -->markers. Full prepare/write/remove/doctor lifecycle. OpenCode uses the XDG-style~/.config/opencode/location on Windows as well; it does not use%APPDATA%\opencode. -
Path safety (
opencode-path-safety.ts): OpenPets never follows or overwrites symlinks when writing OpenCode config or instruction files. Atomic temp-file + rename writes stay inside the validated root with backups, and symlink/parent rejections name the offending path, its resolved target when safely obtainable, and the project-local remediation. To fix a symlinked global config, materialize the symlink as a regular file or use project-local setup instead. -
CLI surfaces:
openpets configure --agent opencodewrites project-local.opencode/files by default;openpets configure --agent opencode --globalwrites the shared global config through the same preparation path as desktop setup.openpets doctorreports global OpenCode status read-only and exits non-zero on genuine diagnostic errors. -
Runtime (
plugin.ts+opencode-plugin-runtime.ts, plugin idopen-pets-opencode): uses the OpenCode 2.0 V2Plugin.define({ id, setup })contract. Setup reads plugin options fromctx.options, subscribes to public V2 event envelopes (type+data) through an abortable async iterator, and registerssession.hook('prompt')plustool.hook('execute.before'/'execute.after'). Prompt starts thinking; before-tool classifies edit/write and test commands; failed foreign tools andsession.execution.failedemit error; OpenPets' own MCP tool failures are ignored; permission requests wait; andsession.idleemits success. Prompt and tool dispatches are serialized so they share one lease acquisition. Unload aborts the event stream, disposes hook registrations, waits for an in-flight acquisition, and releases that lease. Work that was only queued does not acquire after unload. The lease is renewed with a 2s buffer, and the same throttle windows as Claude apply. The optionalexcludeReactionsoption is an array of reactions to suppress before IPC or throttling. OpenCode 2.0 reads it from a nativepluginsentry, including without apettarget:{ "package": "@open-pets/opencode", "options": { "excludeReactions": ["success", "thinking"] } }OpenCode 2.0 normalizes a legacy
plugintuple into that same shape. Currentopenpets configurestill writes the tuple; the options object the plugin reads is the same either way. Invalidpetvalues fail setup instead of being dropped; unrecognized exclusion names are ignored. -
Windows detection: the desktop app checks user and global Scoop shim directories before falling back to
opencode/opencode.cmdonPATH, so a Scoop installation is detectable even when Electron inherited an older environment.
Pure file management for Cursor, no runtime hooks (Cursor drives the pet via the
MCP server). It manages the openpets entry in mcp.json (global
~/.cursor/mcp.json or project .cursor/mcp.json) and optional project rules at
.cursor/rules/openpets.mdc. Strong safety posture: strict JSON only, size caps
(256 KiB config / 64 KiB rules), symlink rejection at every path level, atomic
writes with backup, recursive redaction of sensitive keys/values, and refusal of
unpinned versions (@latest). Rules ownership requires an exact
OPENPETS:CURSOR_RULES:START/END marker pair. The desktop uses preview/copy;
the CLI writes project rules.
Pure global settings management for Zed, with no runtime hooks of its own. It
manages the openpets entry in Zed's global settings.json under
context_servers, using JSONC-aware targeted edits so comments, trailing
commas, unrelated settings, and other context servers survive. The settings
path is ~/.config/zed/settings.json on macOS, $XDG_CONFIG_HOME/zed/settings.json
on Linux/FreeBSD (with FLATPAK_XDG_CONFIG_HOME taking precedence), and
%APPDATA%\Zed\settings.json on Windows.
The package supports pinned published MCP commands (npx -y @open-pets/mcp@VERSION), plus local and bundled Node entry paths (using the
desktop's configured Node.js command when applicable). It reports
missing, installed, disabled, needs-update, conflict, invalid, and
error states, refuses unsafe or ambiguous mutations, strips unsupported
remote execution during correction while preserving managed environment and
timeout fields, and requires explicit replacement to re-enable a disabled entry.
Configured Node.js paths are validated through their resolved executable but
stored in their original safe form so version-manager symlinks continue to
follow upgrades.
Planned writes are rejected when the settings file changes before execution.
The desktop Control Center manages the same status-aware lifecycle through
apps/desktop/src/agent-setup.ts; the CLI
manages the global file with openpets configure --agent zed. Both paths use
the package's journaled, crash-recoverable write APIs rather than editing JSONC
directly. Existing settings remain in place while their verified backup is
prepared; publication uses a temporary claim and no-clobber hard link, and an
interrupted claim is restored from the journal on the next attempt. Ambiguous
recovery state is rejected rather than guessed.
Global MCP config and lifecycle-hook management for Devin.
Devin Desktop (the editor formerly called Windsurf) and the Devin CLI read the
same user-scope MCP file, so one managed mcpServers.openpets entry serves
both. The path is $XDG_CONFIG_HOME/devin/mcp_config.json when
XDG_CONFIG_HOME is set, otherwise ~/.config/devin/mcp_config.json on macOS
and Linux, and %APPDATA%\devin\mcp_config.json on Windows. OpenPets does not
write project .devin/mcp_config.json files or the legacy
~/.codeium/windsurf/mcp_config.json.
The entry uses Devin's stdio schema (command + args, no type field): a
pinned npx -y @open-pets/mcp@VERSION [--pet <id>], or for local/bundled
command modes the configured Node.js command running the OpenPets MCP entry
script. Edits are JSONC-aware targeted edits, so comments, trailing commas,
unrelated servers, and other top-level settings survive. Updating an entry
OpenPets already manages changes only command and args (and clears
disabled on replace), so user fields such as env survive. Writes refuse
symlinked paths, back up the previous file byte-for-byte, stage the content in
an exclusive temp file, and are rejected if the file changed after planning
(for example, through devin mcp add), checked right before the atomic rename.
Status is missing,
installed, disabled (a managed entry with "disabled": true, which install
never re-enables silently; replace does), needs-update, conflict (an
openpets server OpenPets does not manage, which needs an explicit replace and
is never removed), invalid, or error.
The two Devin products have separate hook systems, and OpenPets manages both
(devin-hooks.ts), always as the same command,
openpets hook --openpets-managed --agent devin [--pet <id>] (pinned
npx -y @open-pets/cli@VERSION ..., or Node.js plus the bundled/local CLI
entry):
| Product | File | Events (reaction) |
|---|---|---|
| Devin CLI (Devin Local agent) | hooks key of ~/.config/devin/config.json (%APPDATA%\devin\config.json), Claude Code-compatible format, 5 s timeout |
UserPromptSubmit (thinking), PreToolUse matched to edit/write/apply_patch/notebook_edit/exec (editing, or testing for test commands), PermissionRequest (waiting + permission speech), Stop (success) |
| Devin Desktop (Cascade) | ~/.codeium/windsurf/hooks.json |
pre_user_prompt (thinking), post_write_code (editing), pre_run_command (testing for test commands), post_cascade_response (success) |
devin-hook-events.ts maps both payload families from event and tool names
only; prompt, code, response, and output text are never read or forwarded.
The CLI runtime (packages/cli/src/devin-hook.ts) always exits 0, ignores
oversized (>4 MiB) or malformed input, and sends the decision through the
Claude package's shared dispatchHookDecision() (lease, validated canned
speech, throttling). Hook edits reuse the same JSONC-preserving safe-write
transaction as the MCP entry, replace only hooks whose command carries
--openpets-managed --agent devin, and drop emptied event arrays. Hooks
report installed only when every required event holds exactly one current
OpenPets hook. On Windows,
Devin Desktop entries whose command starts with a quoted executable also get a
powershell variant using the call operator.
Devin CLI also loads Claude Code hooks from ~/.claude/settings.json by
default (read_config_from.claude). If OpenPets Claude Code hooks are installed
too, both fire in Devin CLI; the shared throttle file suppresses most
duplicate reactions.
The desktop Control Center manages MCP and hooks through
apps/desktop/src/agent-setup-devin.ts (hooks are an optional section of the
Devin dialog; install/remove apply to both products). The CLI configures MCP
and both hook files with openpets configure --agent devin (--force
replaces conflicting or disabled MCP entries). After a change, refresh MCP
servers in Devin Desktop or start a new Devin CLI session or Desktop
conversation.
A Pi coding-agent extension (declared in pi.extensions). It maps Pi lifecycle
events (session_start, agent_start, turn_start, …) to reactions and
registers a /openpets slash command namespace (status, test,
react <reaction>, say <message>). MVP scope is default-pet-only and
non-blocking; it registers no model-callable tools, and never forwards
prompt/assistant/tool/command text, paths, URLs, or secrets.
OpenClaw is a native OpenClaw plugin, not an OpenPets SDK v3 catalog plugin and
not an MCP configuration. The published package contains the compiled runtime
and the native openclaw.plugin.json manifest. Its package metadata points
OpenClaw at dist/index.js through openclaw.extensions; the default export is
the openpets plugin entry created with OpenClaw's definePluginEntry().
The manifest declares startup activation and an empty inline configuration
schema, so OpenClaw can inventory the plugin before loading its runtime.
The plugin registers only these OpenClaw lifecycle hooks. Both handlers are argument-free by contract; the integration never reads model, tool, prompt, result, path, or other hook payloads.
| OpenClaw hook | OpenPets effect |
|---|---|
model_call_started |
thinking: a curated thinking speech may be sent, subject to the speech cooldown; otherwise the thinking reaction is sent. |
before_tool_call |
working reaction. |
Dispatch is scheduled and non-blocking. It uses a local-only OpenPets client with bounded connection/response timeouts, and automatic failures do not reach OpenClaw. There are no terminal success or error hooks: the integration does not emit success/error feedback when a model or tool call finishes. It also does not forward arbitrary OpenClaw text to the pet.
OpenPets manages the owned package at the exact workspace package version. The underlying OpenClaw commands are:
openclaw plugins install npm:@open-pets/openclaw@<version>
openclaw plugins enable openpets
openclaw plugins update @open-pets/openclaw@<version>
openclaw plugins uninstall openpets --forceThe install must remain an npm-tracked @open-pets/openclaw installation. A
different source using the openpets id is a conflict rather than something
OpenPets silently replaces.
Use OpenClaw's inventory commands when inspecting the installation directly:
openclaw plugins list --json
openclaw plugins inspect openpets --json
openclaw plugins inspect openpets --runtime --jsonlist and ordinary inspect are cold inventory reads. They can establish what
OpenClaw's persisted registry and manifest metadata say, but cold inventory
cannot prove that a running Gateway loaded the plugin. inspect --runtime
performs a separate runtime inspection pass; after install, update, enablement,
or entry/config changes, restart the Gateway and verify the actual Gateway
process separately.
The desktop and CLI classify the same OpenClaw observations into these states:
An OpenClaw executable installed in a nonstandard location is supported when
the user supplies its absolute executable path in the Control Center. That is
different from a nonstandard plugin source: the openpets id remains owned
only when its tracked install is the @open-pets/openclaw npm package.
| State | Meaning |
|---|---|
installed-enabled / installed-disabled |
The owned npm installation is present and OpenClaw reports its enablement. |
not-installed |
OpenClaw is supported and the owned plugin is absent. |
unavailable |
The configured OpenClaw executable is missing or does not report a version. |
unsupported-host |
The host is outside the supported desktop platforms, or OpenClaw is older than the supported 2026.7.1 minimum. |
management-disabled |
OpenPets itself sees OPENCLAW_NIX_MODE=1; Nix owns the plugin files/configuration, so OpenPets does not mutate them. OpenClaw currently exposes no machine-readable Nix-mode status to a separate parent process. |
conflict |
Plugin id openpets is present but its source is not the tracked npm @open-pets/openclaw package. |
invalid |
OpenClaw returned incomplete plugin metadata or reports a load/dependency problem for the owned installation. |
indeterminate |
A list/inspect or post-action status refresh failed or returned malformed data; no mutation should be retried based on this state alone. |
The Control Center is the desktop-owned setup surface. The main process
resolves the configured openclaw executable, reads version/list/inspect
status, presents command previews, and owns install, update, enable-as-part-of-
setup, and remove actions through the agent-setup IPC boundary. It reports known Nix,
unsupported, conflict, invalid, and indeterminate states instead of treating
them as installable.
The OpenPets CLI exposes the global ensure flow as:
openpets configure --agent openclawOnly --yes is meaningful for this target. OpenClaw setup does not accept
--cwd, --pet, --force, --local-dev, or Cursor rules flags. The CLI
discovers version/list/inspect, plans install-or-update followed by enable, and
refreshes status after each command before declaring success. It has no separate
OpenPets list, inspect, or remove subcommand for OpenClaw; use the native
OpenClaw commands above for direct inventory or lower-level lifecycle control.
OpenClaw remains the owner of its plugin registry and Gateway restart lifecycle.
Install the strict local-only v1 DSH plugin for a DSH profile:
dsh plugin --profile <profile> add @open-pets/dshOr mount into user configuration patch (~/.dsh/cordis.patch.yml):
- insert:
- id: plugin-openpets-dsh
name: '@open-pets/dsh'
disabled: false
config:
enabled: true
minSpeechIntervalMs: 1200
enableStatusLineStream: true
enableToolEvents: true
enableApprovalEvents: trueThe plugin can be toggled on or off at the Cordis loader level (disabled: true/false) or dynamically via plugin config (enabled: false/true).
@open-pets/dsh is a Cordis bundle that automatically maps DSH lifecycle
events to OpenPets reactions:
DSH always uses local IPC and the default pet, and ignores remote configuration.
| DSH event | OpenPets reaction | Speech / Behavior |
|---|---|---|
running (agent/status) |
thinking |
Curated thinking speech |
idle (agent/status) |
success |
Curated success speech (suppressed for 5s after error) |
error (agent/error) |
error |
Curated error speech |
approval/request |
waiting |
Curated permission speech |
agent/assistant-stream |
working |
Sanitized ⏵ status line |
tools/result (bash) |
running |
Sanitized tool description |
tools/result (edit, write) |
editing |
Sanitized file action |
tools/result (read) |
working |
Sanitized file read action |
Automatic dispatch is non-blocking and uses only curated categorical speech or sanitized status text. The bundle never forwards prompts, full tool payloads, raw results, full paths, URLs, secrets, or arbitrary event text. Idle/success is briefly suppressed after an error so the error state remains visible. DSH adds no model tools or MCP setup; the existing OpenPets MCP integration remains separately configurable.
The user-facing front door (openpets), and the package that composes the
others. Commands:
| Command | Does |
|---|---|
configure |
Configure Claude / OpenCode / Cursor for a project, OpenCode globally with --global, Zed or Devin globally, or ensure the global OpenClaw plugin is installed and enabled |
doctor |
Read-only diagnostics for Claude hooks, Cursor project MCP, global OpenCode setup, and app reachability (human and --json output) |
install <pet-id> |
Install a pet via the client |
status |
Print app/pet status JSON over IPC |
pets |
List installed pets |
react <reaction> / say <message> |
Drive the active pet |
mcp |
Launch the MCP stdio server |
hook |
Run a Claude Code lifecycle hook, or a Devin CLI / Devin Desktop hook with --agent devin |
plugin validate <dir> |
Validate a plugin before install/release |
plugin new <name> --template <t> |
Scaffold an SDK v3 plugin |
The plugin subcommands are the author-side DX entry point - see Plugin platform, Plugin SDK v3, and Development. The CLI enforces safe project paths and atomic config writes throughout.
The CLI's status, react, and say commands also work with an explicitly
configured remote client because they use the shared client factory. Remote
credentials are consumed in memory and are not included in command output.
The MCP wrapper skips local lease setup in remote mode and sends only the three
remote-allowlisted operations; local users with no remote configuration retain
discovery-based behavior unchanged.
| Agent | Config home | Runtime mechanism |
|---|---|---|
| Claude Code | ~/.claude/ (settings, MCP, openpets.md) |
lifecycle hooks |
| MCP (generic) | agent's MCP config | stdio MCP tools |
| OpenCode | .opencode/ or ~/.config/opencode/ |
plugin event hooks |
| Cursor | .cursor/mcp.json + rules |
MCP tools |
| Zed | ~/.config/zed/settings.json (platform-specific) |
MCP tools |
| Devin Desktop + Devin CLI | ~/.config/devin/mcp_config.json, ~/.config/devin/config.json, ~/.codeium/windsurf/hooks.json |
MCP tools + lifecycle hooks |
| Pi | pi.extensions |
extension events + /openpets |
| OpenClaw | OpenClaw plugin registry | native plugin hooks; local-only default-pet reactions |



