A native macOS app for real-time monitoring of Claude Code sessions.
git clone https://github.com/onorbumbum/claude-code-watcher.git
cd claude-code-watcher
npm install
npm startOn first launch, click "Setup Monitoring" to install the hooks. Then run any Claude Code command to see it appear in the dashboard.
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/Claude Code Watcher.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 in headless mode (via n8n automation or CLI), there's no visibility into what Claude is doing - no way to see its thinking, tool calls, or responses in real-time.
Solution: This Electron app reads Claude Code's transcript files directly from disk and displays them in a live-updating dashboard. No server required.
┌─────────────────────────────────────────────────────────────────┐
│ Claude Code │
│ (running headless via n8n, CLI, or any automation) │
└─────────────────────┬───────────────────────────────────────────┘
│ writes
▼
┌─────────────────────────────────────────────────────────────────┐
│ ~/.claude/ │
│ ├── projects/[project]/[session-id].jsonl ← transcripts │
│ ├── active-sessions.json ← session registry │
│ ├── settings.json ← hooks config │
│ └── monitor.js ← hook script │
└─────────────────────┬───────────────────────────────────────────┘
│ reads (via Node.js fs)
▼
┌─────────────────────────────────────────────────────────────────┐
│ Claude Code Watcher.app │
│ ├── main.js ← Electron main process, IPC handlers │
│ └── index.html ← Dashboard UI, polls via IPC │
└─────────────────────────────────────────────────────────────────┘
Claude Code hooks in ~/.claude/settings.json trigger monitor.js on every tool call:
{
"hooks": {
"PreToolUse": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "node \"$HOME/.claude/monitor.js\" pre_tool" }] }],
"PostToolUse": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "node \"$HOME/.claude/monitor.js\" post_tool" }] }],
"Stop": [{ "matcher": "*", "hooks": [{ "type": "command", "command": "node \"$HOME/.claude/monitor.js\" stop" }] }]
}
}monitor.js maintains ~/.claude/active-sessions.json:
[
{
"id": "abc123-...",
"path": "/Users/.../.claude/projects/.../abc123.jsonl",
"name": "project-folder-name",
"lastSeen": 1705234567
}
]Sessions expire after 5 minutes of inactivity (cleaned up when new sessions run).
Each line in a transcript .jsonl file is a JSON object:
{"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": "..."}]}, ...}Content can be:
type: "text"- Claude's response texttype: "thinking"- Claude's reasoning (extended thinking)type: "tool_use"- Tool call with name and inputtype: "tool_result"- Result from tool execution
The Electron app:
- Polls
active-sessions.jsonevery 5 seconds - Polls each session's transcript every 2 seconds
- Renders messages with newest first
- Shows tabs for multiple concurrent sessions
- Setup Screen: First-run experience that automatically installs monitoring hooks
- Settings Menu: Install/uninstall hooks, switch themes
- Dark/Light Theme: Toggle between themes via settings dropdown
- Multi-Session Tabs: Monitor multiple concurrent Claude Code sessions
- Live Updates: Real-time polling with pause/resume controls
~/.claude-monitor/
├── package.json # App config, build scripts
├── main.js # Electron main process
│ # - Creates BrowserWindow with secure config
│ # - IPC handlers: check-setup, run-setup, run-uninstall,
│ # read-sessions, read-transcript
│ # - Embedded monitor.js script content
├── preload.js # Secure context bridge for IPC
├── index.html # Dashboard UI (single file, ~900 lines)
│ # - Styles (CSS variables, dark/light themes)
│ # - Setup screen for first-run
│ # - Settings dropdown (hooks, theme)
│ # - Rendering functions (messages, tools, tabs)
│ # - Polling logic (fetchSessions, fetchTranscript)
├── 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-arm64/
│ └── Claude Code Watcher.app
└── README.md # This file
~/.claude/
├── settings.json # Claude Code config with hooks
├── monitor.js # Hook script that updates session registry (Node.js)
├── active-sessions.json # Registry of active sessions
├── events.jsonl # Raw hook events (backup/debug)
├── current-transcript.txt # Path to most recent session
└── projects/
└── [project-path]/
└── [session-id].jsonl # Full conversation transcript
cd ~/.claude-monitor
npm startnpm run build:dir
# Output: dist/mac-arm64/Claude Code Watcher.appcp -R dist/mac-arm64/"Claude Code Watcher.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'),
// ...
});
// Renderer (index.html) - uses the exposed API
const data = await api.readSessions();
// Main process (main.js) - handles the request
ipcMain.handle('read-sessions', async () => {
return fs.readFileSync(path.join(CLAUDE_DIR, 'active-sessions.json'), 'utf8');
});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 active sessions JSONread-transcript- Returns transcript file content
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 = [];Sessions are cleaned from active-sessions.json when:
- A new hook event fires (Node.js filters out sessions older than 5 min)
- The dashboard ignores sessions not in the registry
No cron job needed - cleanup happens organically.
On first launch, the app shows a setup screen that:
- Creates
~/.claude/monitor.js(embedded in main.js) - Merges monitoring hooks into
~/.claude/settings.json - Preserves any existing hooks/settings
Hooks can be installed/uninstalled anytime via the settings menu.
- Check
~/.claude/active-sessions.jsonexists and has content - Verify hooks are configured in
~/.claude/settings.json - Run a Claude Code command to trigger hooks
- Check the transcript file exists at the path shown
- Verify transcript has
type: "user"ortype: "assistant"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
Potential enhancements (not implemented):
- Search/filter messages
- Export session to markdown
- Token usage display
- Cost tracking
- Notification when session completes
- Electron 28.x - Cross-platform desktop app framework
- electron-builder - Packaging for macOS .app
- Node.js fs - Reading transcript files
Note: 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 setup)
- Hooks are the source of truth - If sessions aren't appearing, check hooks first
- 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 - It's rebuilt from hook events, not persisted long-term
- UI is single-file - All styles, scripts, and markup in index.html for simplicity
- Setup is automatic - The app installs its own hooks on first run
- Settings menu - Provides install/uninstall hooks and theme toggle
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)
- No server (Electron reads filesystem directly)
- No external dependencies for the hook script (pure Node.js)
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.

