Milo is a desktop pet companion for Claude Code and Codex CLI. It sits as a transparent overlay in the bottom-right corner of your screen and reacts to agent activity in real time β barking, showing speech bubbles, and playing sounds as your coding assistant edits files, runs commands, and searches the web.
git clone https://github.com/pedrogomez/milo.git
cd milo && ./scripts/install.shOptional provider mode:
./scripts/install.sh --provider codex
./scripts/install.sh --provider claude
./scripts/install.sh --provider bothThe install script will:
- Verify prerequisites (Node.js >= 20, Rust, npm)
- Build the Milo event server
- Optionally install Claude hooks into
~/.claude/settings.json(when provider includesclaude) - Register the event server as a macOS Launch Agent (starts automatically at login)
- Build the Milo desktop app and copy
Milo.appto/Applications(macOS)
1. Clone the repository
git clone https://github.com/pedrogomez/milo.git
cd milo2. Build and configure the event server
cd event-stream
npm install
npm run build
npm run install-hooks # required only for Claude
MILO_EVENT_PROVIDERS=both npm run install-service # claude | codex | both3. Build and run the Milo desktop app
cd pixijs/milo
npm install
npm run tauri:build
open src-tauri/target/release/bundle/macos/Milo.appThe event server starts automatically at login β no terminal needed. Just open Milo:
open /Applications/Milo.appTo manage the event server service:
cd event-stream
npm run service-status # check if the service is running
npm run uninstall-service # stop and remove the Launch Agent
MILO_EVENT_PROVIDERS=both npm run install-service # re-install with provider modeServer logs are written to ~/Library/Logs/Milo/.
Use the tray menu:
- Uncheck Lock position (click-through)
- Drag Milo to any position
- Check Lock position (click-through) again to keep the new position and restore click-through mode
Use Reset to bottom-right anytime to snap Milo back to the default corner.
+--------------+ stdin/JSON +----------+ HTTP POST +--------------+
| Claude Code | -----------> | hook.js | -----------> | |
| (hooks) | | | | |
+--------------+ +----------+ | server.js | WebSocket +------------------+
| :6271 | -----------> | Milo (Tauri + |
+--------------+ JSONL tail +---------------------+ | | | PixiJS overlay) |
| Codex CLI | ------------> | ~/.codex/sessions/* |->| | +------------------+
| (sessions) | | (polling tailer) | +--------------+
+--------------+ +---------------------+
- Node.js >= 20
- Rust stable >= 1.77.2 (
rustup default stable) - Tauri CLI (installed as a dev dependency via npm)
- npm
cd pixijs/milo
npm installcd event-stream
npm install
npm run build
# Claude only
npm run install-hooks
MILO_EVENT_PROVIDERS=claude npm start
# Codex only
MILO_EVENT_PROVIDERS=codex npm start
# Both
# (install Claude hooks once, then run both providers)
npm run install-hooks
MILO_EVENT_PROVIDERS=both npm startFor Codex, Milo tails ~/.codex/sessions JSONL files. Codex sessions started with --ephemeral are not persisted, so they cannot be streamed via file tailing.
See event-stream/README.md for full backend details.
cd pixijs/milo
npm run tauri:devThis starts the Vite dev server (port 8080) and opens the Tauri window β a transparent, frameless overlay pinned to the bottom-right of your screen. Changes to the TypeScript/PixiJS code hot-reload automatically.
cd pixijs/milo
npm run tauri:buildOutput:
pixijs/milo/src-tauri/target/release/bundle/macos/Milo.apppixijs/milo/src-tauri/target/release/bundle/dmg/Milo_0.1.0_aarch64.dmg
open pixijs/milo/src-tauri/target/release/bundle/macos/Milo.appcd pixijs/milo
npm testcd pixijs/milo
npm run lint
npx prettier --check src/
npx tsc --noEmitThe frontend TypeScript codebase follows a clean architecture with three layers:
pixijs/milo/src/
domain/ # Pure business logic, no external dependencies
types.ts # Core types (MiloMessage, AgentEvent, configs)
message-queue.ts # FIFO message queue with listener support
event-classifier.ts # Classifies provider events into Milo messages
reminder-scheduler.ts # Reminder timing calculations and state management
text.ts # Text processing (markdown stripping, truncation)
infrastructure/ # External system adapters
agent-event-stream.ts # WebSocket client for Milo event server
tauri-reminders.ts # Bridges Tauri events to the reminder manager
bark-audio.ts # Audio playback and mute state via Tauri events
animations.ts # PixiJS animation state machine (idle/bark)
bubble.ts # PixiJS speech bubble rendering
main.ts # Composition root: wires everything together
random.ts # No-repeat random picker utility
session-names.ts # Maps session IDs to funny display names
i18n/ # Localization (en, es, fr, de, it, pt, zh, ja)
Domain layer β Pure functions and classes with no dependencies on Tauri, WebSocket, or PixiJS. All domain logic is independently testable.
Infrastructure layer β Adapts external systems (Tauri event API, WebSocket, HTML Audio) to domain interfaces.
Composition root (main.ts) β Initializes PixiJS, wires domain and infrastructure together, and runs the animation/message display loop.
milo/
event-stream/ # Event stream server (Claude + Codex adapters)
src/
types.ts # Canonical event types and server config
hook.ts # Claude hook handler β reads stdin, POSTs to server
server.ts # WebSocket + HTTP server (port 6271)
install.ts # Installs/uninstalls hooks in Claude settings
providers/ # Provider adapters (claude, codex)
bin/ # CLI entry points
pixijs/
art/ # Source animation frames
milo/ # Tauri v2 + PixiJS desktop app (see Architecture above)
public/assets/ # Animation frames + sound files
src-tauri/ # Tauri v2 Rust backend
src/lib.rs # Window setup (transparent, click-through)
tauri.conf.json
tools/
sprite-extractor/ # Extracts animation frames from video files
.github/workflows/ci.yml # CI pipeline (lint, format, typecheck, test, build)
- Claude Code emits hook events piped to
hook.js, which forwards them to the server. - Codex CLI writes session JSONL files in
~/.codex/sessions, and the server tails them. - The event server (
event-stream/) normalizes provider events, stores them in a ring buffer, and broadcasts them over WebSocket. - Milo (
pixijs/milo/) connects to the WebSocket stream, classifies each event, queues a message, and triggers:- A bark animation πΎ (randomly chosen, no repeats)
- A speech bubble π¬ with context about what the coding agent is doing
- A bark sound effect π (randomly chosen, no repeats)
- Between events, Milo plays idle animations in a loop.
- Reminders π fire on configurable schedules (daily or hourly intervals) and display with a bell prefix in the speech bubble to differentiate them from agent activity messages.
The desktop overlay is configured as:
- Transparent background (no visible window chrome)
- Frameless (no title bar or decorations)
- Always on top of other windows
- Click-through enabled (clicks pass through transparent areas)
- Hidden from Dock/taskbar
- Positioned at the bottom-right corner of the screen
| Command | Description |
|---|---|
npm run tauri:dev |
Development mode with hot reload |
npm run tauri:build |
Build the production .app bundle |
npm run dev |
Run PixiJS frontend only (browser) |
npm test |
Run tests |
npm run lint |
Run ESLint |
npx prettier --check src/ |
Check formatting |
npx tsc --noEmit |
Type check |
| Command | Description |
|---|---|
npm start |
Start the event stream server |
npm run install-hooks |
Register hooks in Claude Code settings |
npm run uninstall-hooks |
Remove hooks from Claude Code settings |
npm run install-service |
Register Launch Agent (auto-start at login) |
npm run uninstall-service |
Stop and remove the Launch Agent |
npm run service-status |
Check if the service is running |
npm test |
Run backend adapter tests |
Environment variables for npm start / service install:
MILO_EVENT_PROVIDERS(claude,codex,both; defaultclaude)CLAUDE_EVENT_PORT(default6271)CODEX_SESSIONS_DIR(default~/.codex/sessions)CODEX_POLL_MS(default2000)
- Pedro Vicente GΓ³mez SΓ‘nchez - pedrovicente.gomez@gmail.com
- Does your use Milo? Just tell me if this is the case!
Copyright 2026 Pedro Vicente GΓ³mez SΓ‘nchez
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

