- Rename all user-facing and technical identifiers from OpenPets/Pet to FamiliarOS/Familiar. - Rename packages from @open-pets/* to @familiaros/*; rename install-pet/pet-format packages. - Rename plugin IDs and directories from openpets.* to familiaros.*. - Rename IPC namespace from openpets:* to familiaros:* and state filenames from openpets-* to familiaros-* with legacy migration. - Rename source files (pet-window, built-in-pet, default-pet-controller, etc.) to familiar equivalents. - Update locales (en, es-419, ja, ko, pt-BR, zh-Hans, zh-Hant) and tray/pet context menu strings. - Add Familiar naming feature: preference, settings input, tray menu display. - Update assets and packaging config; all desktop tests pass.
12 KiB
FamiliarOS reaction, speech, and Claude hook mapping
This document explains how FamiliarOS decides which familiar reaction is shown, when the familiar says something, and how Claude Code hooks and OpenCode plugin events map to reactions/messages.
Important terms
- Reaction: one of the allowed FamiliarOS reaction names, for example
thinking,success, orerror. - Speech/message: a short text bubble sent with
familiaros_sayor generated by a Claude hook. - Hook speech: automatic short speech generated by Claude Code hook events.
- Target familiar: the familiar that receives the reaction/message. This is either the desktop default familiar or a project/agent-selected familiar through a lease.
Allowed reactions
FamiliarOS currently accepts these reactions:
| Reaction | Intended meaning |
|---|---|
idle |
Neutral / no special task state. |
thinking |
The agent is thinking or starting to process a prompt. |
working |
Generic tool/work activity. |
editing |
The agent is editing or writing files. |
running |
The agent is running a command that is not classified as tests. |
testing |
The agent is running tests. |
waiting |
The agent is waiting for permission or another blocking state. |
waving |
The familiar waves for notifications/attention. |
success |
The agent completed successfully. |
error |
The agent failed or hit a problem. |
celebrating |
Positive manual reaction; not currently emitted by Claude hooks. |
Source of truth: packages/client/src/protocol.ts and apps/desktop/src/local-ipc-protocol.ts.
What a reaction visually does today
Reactions drive the familiar spritesheet animation and can also show temporary bubble text above the familiar. For example, thinking uses the review/thinking animation row and, when sent as a reaction-only event, displays a short randomized status line such as Thinking it through or Checking the clues.
The defaults below are defined in apps/desktop/src/reaction-animation-mapping.ts. Users can override reaction-to-animation defaults in Settings → Reaction animations. Overrides apply globally to the default familiar and explicit/agent familiar windows. Drag-only rows (running-left, running-right) are never user-selectable because they are controlled by familiar movement.
The universal Codex/FamiliarOS spritesheet has these animation rows:
| Row | Spritesheet state | Trigger/reaction mapping |
|---|---|---|
| 0 | idle |
No active reaction, message-only speech, or idle. Slow 5500ms loop. |
| 1 | running-right |
Drag right via motion state run-right. |
| 2 | running-left |
Drag left via motion state run-left. |
| 3 | waving |
waving, Claude Notification. |
| 4 | jumping |
success, celebrating. |
| 5 | failed |
error. |
| 6 | waiting |
waiting, testing, PermissionRequest. |
| 7 | running |
working, editing, running. |
| 8 | review |
thinking. |
Drag motion overrides reaction animation while the familiar is being moved. When dragging stops, the sprite returns to the active reaction animation until the transient display clears, then returns to idle.
One-shot feedback animations do not loop for the full 4-second bubble lifetime:
| Spritesheet state | Iterations |
|---|---|
waving |
2 loops |
jumping |
2 loops |
failed |
2 loops |
Long-running states (idle, waiting, running, review, and drag run states) continue looping while active. If a user override maps a reaction to a different animation state, the selected state's one-shot or looping behavior applies.
Bubble priority
When a transient display has both a message and a reaction, the message wins.
| Input | Bubble text shown |
|---|---|
Reaction only: familiaros_react({ reaction: "testing" }) |
A stable randomized status line from the testing message pool, for example Running the checks. |
Message only: familiaros_say({ message: "Done" }) |
Done |
Message + reaction: familiaros_say({ message: "Done", reaction: "success" }) |
Done |
The reaction is still sent with the message, but the visible bubble text is the message.
Source of truth: apps/desktop/src/familiar-window.ts (createBubbleMarkup).
How long bubbles stay visible
Reaction/message bubbles are transient and clear after a length-aware duration:
| Bubble type | Duration |
|---|---|
| Most reactions/messages | At least 4 seconds. |
success / error reactions |
At least 5 seconds. |
| Longer explicit messages | Extended by message length, capped at 12 seconds. |
Source of truth: apps/desktop/src/familiar-window.ts (getTransientDisplayDurationMs).
Claude hook event mapping
When FamiliarOS Claude hooks are installed, these Claude Code hook events are registered:
UserPromptSubmit
PreToolUse
PermissionRequest
Notification
Stop
StopFailure
Source of truth: packages/claude/src/hook-settings.ts.
Hook event to reaction/message table
| Claude hook event | Condition | Reaction sent | Does it speak? | Possible speech text |
|---|---|---|---|---|
UserPromptSubmit |
Any user prompt submitted. | thinking |
Yes, throttled. | Thinking it through, Let me check, On it, Working it out |
PreToolUse |
Tool is Edit, Write, or MultiEdit. |
editing |
No. | — |
PreToolUse |
Tool is Bash and command looks like a test command. |
testing |
No. | — |
PreToolUse |
Tool is Bash and command does not look like a test command. |
running |
No. | — |
PreToolUse |
Any other tool. | working |
No. | — |
PermissionRequest |
Claude asks for permission/approval. | waiting |
Yes, shorter cooldown. | Approval needed |
Notification |
Claude sends a notification. | waving |
No. | — |
Stop |
Claude completes normally. | success |
Yes, throttled. | Done, That worked, All set, Nice, finished |
StopFailure |
Claude run fails/stops with failure. | error |
Yes, throttled. | Something failed, Needs another look, Hit a snag, Not quite there |
| Unknown event | Hook event name exists but is not recognized. | None. | No. | — |
| Invalid/missing event | Payload is invalid or has no event name. | None. | No. | — |
Source of truth: packages/claude/src/hooks.ts and packages/claude/src/hook-messages.ts.
Bash test detection
For PreToolUse with tool_name: "Bash", FamiliarOS classifies the command as testing if the command contains one of these patterns:
test
vitest
jest
pytest
npm test
pnpm test
yarn test
cargo test
go test
Otherwise Bash maps to running.
Hook speech throttling
Hooks do not speak on every matching event. Speech is throttled so the familiar does not spam bubbles.
| Speech category | Events that use it | Cooldown |
|---|---|---|
thinking |
UserPromptSubmit |
20 seconds |
success |
Stop |
20 seconds |
error |
StopFailure |
20 seconds |
permission |
PermissionRequest |
3 seconds |
If speech is throttled, FamiliarOS still sends the reaction with familiar.react. Example: if Stop happens twice within 20 seconds, the second one sends success as a reaction only instead of saying another success phrase.
Source of truth: packages/claude/src/hooks.ts (speechCooldownMs, permissionCooldownMs, shouldSendSpeech).
OpenCode plugin event mapping
When FamiliarOS OpenCode setup is installed, OpenCode loads the @familiaros/opencode plugin. Plugin hooks return immediately and schedule FamiliarOS IPC calls in the background so OpenCode is not blocked by familiar activity.
| OpenCode hook/event | Condition | Reaction sent | Does it speak? |
|---|---|---|---|
chat.message |
User/chat activity. | thinking |
Yes, throttled. |
tool.execute.before |
Edit/write/patch-like tool name. | editing |
No. |
tool.execute.before |
Shell/bash-like tool name and test-like command category. | testing |
No. |
tool.execute.before |
Shell/bash-like tool name, not test-like. | running |
No. |
tool.execute.before |
Other tools. | working |
No. |
event |
Bus event permission.asked. |
waiting |
Yes, short approval-needed speech. |
| Internal/self FamiliarOS tools | FamiliarOS MCP/status/say/react calls. | None. | No. |
OpenCode speech uses the same safe shared message pools as Claude hooks. It never includes prompt text, commands, output, code, logs, paths, URLs, or secrets.
Source of truth: packages/opencode/src/opencode-plugin-runtime.ts and packages/agent-events/src/index.ts.
Hook speech safety rules
Hook-generated speech must be safe before it is sent. It must be:
- 1–140 characters.
- Single-line.
- Not code-like.
- Not a URL.
- Not a path.
- Not secret-like.
If a generated speech message fails validation, the hook ignores the error and does not break Claude Code.
Source of truth: packages/claude/src/hooks.ts, packages/opencode/src/opencode-plugin-runtime.ts, and packages/agent-events/src/index.ts.
MCP tool mapping
The public MCP tools are intentionally small:
| MCP tool | Input | Effect |
|---|---|---|
familiaros_status |
none | Checks whether FamiliarOS is running and which familiar is targeted. Does not change familiar state. |
familiaros_react |
{ reaction } |
Drives the reaction animation and may show a short randomized temporary bubble from that reaction's message pool. |
familiaros_say |
{ message, reaction? } |
Sends a message bubble, optionally with a reaction. The message is what appears in the bubble. |
MCP message validation
familiaros_say messages must be:
- 1–140 characters after trimming.
- Single-line.
- Not code-like.
- Not a URL.
- Not path-like.
- Not secret-like.
Source of truth: packages/mcp/src/tools.ts and apps/desktop/src/local-ipc-protocol.ts.
Target familiar routing
FamiliarOS decides which familiar gets the reaction/message as follows:
| Situation | Target |
|---|---|
| No selected/configured familiar. | Desktop default familiar. |
| Selected/configured familiar is the built-in familiar or current default familiar. | Desktop default familiar. |
| Selected/configured familiar is installed and not broken. | Explicit agent familiar window for that familiar. |
| Selected/configured familiar is invalid, missing, or broken. | Falls back to desktop default familiar. |
Claude hooks, OpenCode plugin events, and MCP use a short-lived lease when a project/configured familiar is specified. That lease routes say/react to the explicit familiar window.
Source of truth: apps/desktop/src/local-ipc.ts (resolveLeaseTarget), packages/claude/src/hooks.ts (acquireHookLease), and packages/opencode/src/opencode-plugin-runtime.ts.
Paused behavior
If the default familiar is paused:
- Default-familiar reactions/messages are not shown and return
reason: "paused". - Explicit agent familiar reactions/messages also check the default paused state and are not shown when paused.
Source of truth: apps/desktop/src/default-familiar-controller.ts and apps/desktop/src/local-ipc.ts.
Visibility behavior
Default familiar:
- A reaction/message refreshes the default familiar content.
- The default familiar is only shown for external events if it is already visible or
openDefaultPetOnLaunchis enabled.
Explicit agent familiar:
- A reaction/message opens/shows that agent familiar window.
- The agent familiar window is closed when its explicit lease expires and no explicit lease remains.
Source of truth: apps/desktop/src/default-familiar-controller.ts, apps/desktop/src/agent-familiar-controller.ts, and apps/desktop/src/local-ipc.ts.
Current product implications
- Hook emotions now change sprite animation rows; bubbles are still used for short visible text.
- Only some hooks/events make the familiar speak. Claude prompt/permission/stop events and OpenCode chat/permission events can produce speech. Tool-use hooks only react.
- Speech is intentionally short and safe. Hooks/plugins never display prompts, command output, code, logs, paths, URLs, or secrets.
- Messages override reaction text visually. If a message is present, the bubble shows the message, not a randomized reaction line, while the reaction still controls animation.
celebratingexists as an allowed manual reaction but is not emitted by Claude hooks today.