A native macOS app for real-time monitoring of Claude Code and Pi coding agent sessions.
git clone https://github.com/onorbumbum/agent-peeper.git
cd agent-peeper
npm install
npm startOn first launch, click "Setup Monitoring" to install the Claude Code hooks. Pi sessions are discovered automatically — no setup required.
For detailed installation instructions (including Windows/Linux), see INSTALL.md.
If you build and install the app to /Applications, macOS may block it with "app is damaged" or "unidentified developer" warnings. This is because the app isn't notarized with Apple.
Fix it with one command:
xattr -cr "/Applications/AgentPeeper.app"Or: Right-click the app → Open → Click "Open" in the dialog.
| Setup | Dashboard |
|---|---|
![]() |
![]() |
Left: First-run setup automatically installs monitoring hooks. Right: Dashboard ready to monitor sessions.
Problem: When running Claude Code or Pi in headless mode (via automation, CLI, or -p flag), there's no visibility into what the agent is doing — no way to see its thinking, tool calls, or responses in real-time.
Solution: This Electron app reads session transcript files directly from disk and displays them in a live-updating dashboard. No server required. Works with both Claude Code and Pi coding agent.
┌──────────────────────────┐ ┌──────────────────────────┐
│ Claude Code │ │ Pi Coding Agent │
│ (interactive or -p) │ │ (interactive or -p) │
└───────────┬──────────────┘ └───────────┬──────────────┘
│ writes │ writes
▼ ▼
┌──────────────────────────┐ ┌──────────────────────────┐
│ ~/.claude/ │ │ ~/.pi/agent/sessions/ │
│ projects/[proj]/ │ │ [encoded-cwd]/ │
│ [session-id].jsonl │ │ [ts]_[uuid].jsonl │
└───────────┬──────────────┘ └───────────┬──────────────┘
│ reads (fs polling) │ reads (fs polling)
└───────────┬────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ AgentPeeper.app │
│ ├── main.js ← Electron main, IPC, session discovery │
│ ├── preload.js ← Secure context bridge │
│ └── index.html ← Dashboard UI, unified rendering │
└─────────────────────────────────────────────────────────────┘
The watcher uses three discovery mechanisms that run in parallel:
- Claude Code hooks (interactive mode) —
monitor.jshook script maintains~/.claude/active-sessions.jsonon every tool call - Claude Code filesystem polling (headless
-pmode) — scans~/.claude/projects/for.jsonlfiles modified in the last 5 minutes. This catches sessions where hooks don't fire (e.g.,claude -p "prompt") - Pi filesystem polling — scans
~/.pi/agent/sessions/for recently-modified.jsonlfiles. No hooks or setup needed
All discovered sessions are merged and deduplicated. Each session tab shows a source badge: CC (blue) for Claude Code or π (violet) for Pi.
Claude Code transcripts (.jsonl):
{"type": "assistant", "message": {"content": [{"type": "text", "text": "..."}]}, "timestamp": "..."}
{"type": "assistant", "message": {"content": [{"type": "tool_use", "name": "Bash", "input": {...}}]}}
{"type": "user", "message": {"content": [{"type": "tool_result", "content": "..."}]}}Pi transcripts (.jsonl with tree structure):
{"type": "session", "version": 3, "id": "uuid", "cwd": "/path/to/project"}
{"type": "message", "id": "abc", "parentId": null, "message": {"role": "user", "content": [...]}}
{"type": "message", "id": "def", "parentId": "abc", "message": {"role": "assistant", "content": [...]}}
{"type": "message", "id": "ghi", "parentId": "def", "message": {"role": "toolResult", "toolName": "read", "content": [...]}}
{"type": "model_change", "id": "jkl", "parentId": "ghi", "modelId": "claude-opus-4"}Pi entries are normalized to Claude Code format before rendering — toolCall → tool_use, toolResult → tool_result, etc. — so all rendering code is shared.
Pi's tree structure (branching) is resolved by following the latest leaf path.
The Electron app:
- Polls all three discovery sources every 5 seconds
- Polls each session's transcript every 2 seconds
- Renders messages with newest first
- Shows unified tabs with source badges for multiple concurrent sessions
- Multi-Agent Support: Monitor both Claude Code and Pi coding agent sessions
- Headless Mode Support: Filesystem polling catches sessions from
-pmode where hooks don't fire - Setup Screen: First-run experience that automatically installs Claude Code monitoring hooks
- Settings Menu: Install/uninstall hooks, switch themes
- Dark/Light Theme: Toggle between themes via settings dropdown
- Multi-Session Tabs: Monitor multiple concurrent sessions with source badges (
CC/π) - Pi Inline Indicators: Subtle indicators for model changes and thinking level changes
- Live Updates: Real-time polling with pause/resume controls
- Zero Pi Setup: Pi sessions are discovered automatically via filesystem — no hooks or extensions needed
agent-peeper/
├── package.json # App config, build scripts
├── main.js # Electron main process
│ # - Creates BrowserWindow with secure config
│ # - IPC handlers for CC and Pi sessions
│ # - Filesystem scanners for both agents
│ # - Embedded monitor.js script content
├── preload.js # Secure context bridge for IPC
├── index.html # Dashboard UI (single file)
│ # - Styles (CSS variables, dark/light themes)
│ # - Setup screen for first-run
│ # - Settings dropdown (hooks, theme)
│ # - Pi transcript normalization + tree resolution
│ # - Unified rendering (messages, tools, tabs)
│ # - Polling logic (3 discovery sources)
├── logo.png # App icon (PNG)
├── icon.icns # macOS app icon
├── screenshot-1.png # Setup screen screenshot
├── screenshot-2.png # Dashboard screenshot
├── LICENSE # MIT license
├── CONTRIBUTING.md # Contribution guidelines
├── CODE_OF_CONDUCT.md # Community guidelines
├── CHANGELOG.md # Version history
├── dist/ # Built app output (git-ignored)
│ └── mac/
│ └── AgentPeeper.app
└── README.md # This file
~/.claude/ # Claude Code data
├── settings.json # Hooks config
├── monitor.js # Hook script (installed by app)
├── active-sessions.json # Hook-based session registry
└── projects/[project-path]/
└── [session-id].jsonl # CC transcripts
~/.pi/agent/sessions/ # Pi data (read-only)
└── [encoded-cwd]/
└── [timestamp]_[uuid].jsonl # Pi transcripts
npm startnpm run build:dir
# Output: dist/mac-arm64/AgentPeeper.apprm -rf "/Applications/AgentPeeper.app"
cp -R dist/mac-arm64/AgentPeeper.app /Applications/The renderer (index.html) cannot access the filesystem directly. It uses a secure preload script:
// preload.js - exposes safe API via contextBridge
contextBridge.exposeInMainWorld('api', {
readSessions: () => ipcRenderer.invoke('read-sessions'),
readCcSessionsFs: () => ipcRenderer.invoke('read-cc-sessions-fs'),
readPiSessions: () => ipcRenderer.invoke('read-pi-sessions'),
// ...
});Available IPC handlers:
check-setup- Returns setup status (hooks configured, script exists)run-setup- Installs monitor.js and configures hooksrun-uninstall- Removes hooks and monitor scriptread-sessions- Returns hook-based active sessions JSONread-cc-sessions-fs- Returns filesystem-discovered CC sessionsread-transcript- Returns CC transcript file contentread-pi-sessions- Returns filesystem-discovered Pi sessionsread-pi-transcript- Returns Pi transcript file content
Pi uses a different JSONL format than Claude Code. The dashboard normalizes Pi entries to the CC format:
| Pi format | Normalized to CC format |
|---|---|
type: "toolCall" with arguments |
type: "tool_use" with input |
Separate role: "toolResult" message |
Inline type: "tool_result" block |
role: "bashExecution" |
Tool result with command + output |
type: "model_change" |
Inline indicator (↻ Switched to ...) |
type: "thinking_level_change" |
Inline indicator (↻ Thinking: ...) |
type: "compaction" |
Inline indicator (↻ Context compacted) |
Pi sessions use a tree structure with id/parentId for branching. The watcher resolves this by:
- Building a parent → children map
- Finding all leaf nodes (entries with no children)
- Selecting the leaf with the latest timestamp
- Walking from leaf to root to get the active branch
Sessions disappear from the dashboard after 5 minutes of inactivity:
- CC hooks: Cleaned from
active-sessions.jsonwhen new hook events fire - CC filesystem: File mtime checked against 5-minute cutoff
- Pi filesystem: Same mtime-based cutoff
Transcript messages have message.content as either:
- A string (legacy format)
- An array of blocks (modern format)
The dashboard handles both:
let content = msg.message?.content;
if (typeof content === 'string') content = [{ type: 'text', text: content }];
else if (!Array.isArray(content)) content = [];- For CC (interactive): Check hooks are installed via Settings menu
- For CC (headless): Verify
.jsonlfiles exist in~/.claude/projects/and are being written to - For Pi: Verify
.jsonlfiles exist in~/.pi/agent/sessions/ - Sessions expire after 5 minutes — the agent must be actively running
- Check the transcript file exists at the path shown
- Verify transcript has valid JSONL entries
- Check browser console (Cmd+Option+I) for errors
- Verify
~/.claude/monitor.jsexists - Test hook manually:
echo '{"session_id":"test"}' | node ~/.claude/monitor.js pre_tool - Check
~/.claude/events.jsonlfor logged events - Re-install hooks via Settings menu
- Note: Hooks don't fire in
-pmode — the filesystem scanner handles that
- Electron 28.x - Cross-platform desktop app framework
- electron-builder - Packaging for macOS .app
- Node.js fs - Reading transcript files
No external dependencies like jq required — the monitor script is pure Node.js.
When working on this codebase:
- The app reads files, never writes them — it's a passive monitor (except for hook setup)
- Three discovery mechanisms — CC hooks, CC filesystem scan, Pi filesystem scan
- Hooks don't fire in
-pmode — filesystem polling is the fallback for headless sessions - Pi entries are normalized to CC format — all rendering code is shared (DRY)
- Pi tree structure is resolved to follow the latest leaf branch
- Transcript format varies — always handle both string and array content
- No server needed — Electron's Node.js integration reads files directly
- Session registry is ephemeral — rebuilt from hooks/filesystem on each poll
- UI is single-file — all styles, scripts, and markup in index.html for simplicity
- Setup is automatic for CC — hooks installed on first run; Pi needs no setup
The design prioritizes KISS:
- Single HTML file for the entire UI
- No build step for the frontend (no React, no bundler)
- No database (just JSON files on disk)
- No server (Electron reads filesystem directly)
- No external dependencies for the hook script (pure Node.js)
- Pi support requires zero configuration
Contributions are welcome! Please read CONTRIBUTING.md for guidelines and CODE_OF_CONDUCT.md for community standards.
This project is licensed under the MIT License - see the LICENSE file for details.
See CHANGELOG.md for version history.

