Give your AI agent a face
Connected to Claude Code
Waiting for Claude Code
Your AI agent runs in the background. Your mascot shows what it is doing and tells you when it needs you. Each step of the agent sends an event, and each event moves the mascot to a new pose.
20 min · 66 credits · Web, Mac, any agent
1. Make the clips
Start with one five-second loop per state: Idle, Thinking, Working, Needs approval, Done and Error. Send the six requests in one batch call, 11 credits each.
{
"requests": [
{
"type": "animation",
"animation_model": "standard",
"name": "Needs approval",
"image_prompt": "waving one hand to get attention",
"animation_prompt": "waves twice, then waits",
"duration": 5,
"loop": true
}
]
}Add the five other states to requests, then wait for the jobs and keep each clip's urls.webm and urls.hevc. Fun extras like Searching, Found it, Running tests and Deploying are one more request each.
2. Get the events
From Claude Code or Codex
A hook is a command your agent runs at each event, with the event as JSON. Point every event at your app:
{
"hooks": {
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "curl -s -m 1 -X POST localhost:4545/event --data-binary @- || true"
}
]
}
]
}
}Add the same entry for PreToolUse, PostToolUse, PermissionRequest, Notification, Stop, StopFailure and SessionEnd. Codex reads the same format from ~/.codex/hooks.json. Your app reads hook_event_name, and tool_name for tools, and picks the state:
| Event | Tool | State |
|---|---|---|
UserPromptSubmit | Thinking | |
PreToolUse | Grep, Glob, Read | Searching |
PreToolUse | Edit, Write | Working |
PreToolUse | Bash running tests | Running tests |
PreToolUse | Bash running a deploy | Deploying |
PermissionRequest, Notification | Needs approval | |
Stop | Done | |
StopFailure | Error | |
SessionEnd | Idle |
Let a small model catch the moments
The hook says which tool runs, not what the agent found. For moments like "found the bug" or "the build failed", send the agent's last words to a small OpenAI model and let it pick. Claude Code puts them in tool_response and last_assistant_message. With no hooks at all, like a chatbot or your own agent loop, the same call drives every state.
const decision = await openai.decisions.create({
model: 'gpt-6-luna',
input: lastMessages,
questions: [
{
type: 'choice',
name: 'state',
instructions: 'What is the AI agent doing now?',
choices: [
{ value: 'thinking', description: 'Reads, plans or writes.' },
{ value: 'found_it', description: 'Just found the cause of a bug.' },
{ value: 'needs_approval', description: 'Asks the user for a yes.' },
{ value: 'done', description: 'Finished and reports back.' },
{ value: 'error', description: 'Something failed.' }
]
}
]
});It answers in about a third of a second. If it is unsure or slow, keep the current state.
3. Show the right clip
const clips = { thinking: THINKING_URL, working: WORKING_URL, done: DONE_URL };
function show(state) {
mascot.src = clips[state];
mascot.play();
}To switch with no blink, use two players: smooth mascot playback.
