A live dashboard over your tmux sessions. Every window becomes a card; click
into a card to open a full live terminal (xterm.js + tmux pipe-pane over
WebSocket), send keystrokes, focus the window in tmux, or rename it. Identifies
Claude Code and Codex panes alongside ordinary shells, combines their agent
state with branch/PR/CI metadata, and surfaces work that is running or ready
for attention.
Built for the "I have 30 tmux windows across 5 sessions with coding agents running, and I want a single pane of glass" workflow.
tmux(any reasonably modern version)uvfor running the single-file script- Node 20+ +
npm— the frontend is a Vite-built bundle (static/dist/) - A modern browser (uses WebSockets)
- Codex CLI 0.146.0 for the initial, macOS-tested Codex integration
- Optional:
ANTHROPIC_API_KEYfor the ✨ auto-rename feature
npm install && npm run build # one-time / whenever static/src/ changes
uv run server.pyOpen http://127.0.0.1:8765/. Polls every 3s; the modal opens a live WebSocket bridge to the selected pane.
For the always-on / launchd-managed setup and the prod/dev port split,
see CLAUDE.md → Development workflow
and bin/periscope.
bin/periscope install-hook merges Periscope's Claude and Codex lifecycle
hooks into their existing user configuration; it does not replace unrelated
hooks. Provider-specific commands are also available:
bin/periscope install-claude-hook
bin/periscope install-codex-hook
bin/periscope uninstall-claude-hook
bin/periscope uninstall-codex-hookThe experimental Codex definition is written atomically to
$CODEX_HOME/hooks.json (default ~/.codex/hooks.json). After installing,
open /hooks in Codex and review/trust the exact Periscope command. Periscope
does not bypass hook trust. /api/healthz reports definitions and observations,
but cannot infer trust. Codex hook attribution is currently marked
verification: "unresolved" until live root/subagent hook evidence is
captured; recorded bindings are therefore not authoritative state evidence.
If /api/healthz reports the Codex hook as installed but unobserved, first
confirm /hooks trusts that exact command, then start a fresh Codex session and
submit a prompt. Also check that the server and Codex use the same
$CODEX_HOME; an explicit empty value has not yet been verified and should be
avoided. Do not use --dangerously-bypass-hook-trust.
Choose Codex in either launcher to open it in the selected worktree. The
new-tab route also accepts a Codex resume UUID; Periscope validates it against
the local rollout catalog, rejects a UUID already bound to a live Codex pane,
pre-binds the new pane, and runs codex resume <uuid> -C <cwd>.
Codex v1 focuses on working, idle, and the existing derived done
attention state. Missing or contradictory evidence is unknown and must not
produce a completion alert. Codex panes default to the live terminal and do not
offer transcript mode, Claude channels, Claude plan usage, memory warnings,
narrator status, context reset, or AI auto-rename.
Lifecycle hooks are intended to provide pane/session identity once the manual
evidence gate is satisfied. Until then, verified rollout records and the macOS
process/TUI detector are defensive, conservative fallbacks. Interruption,
cancellation, approval round trips, Stop continuation, Linux process layouts,
and root-vs-subagent hook behavior remain manual verification items; ambiguous
cases stay unknown.
For hot-reload while editing static/app.js or static/styles.css:
npm install # one-time
npm run dev # then visit http://127.0.0.1:5174/npm run dev runs the FastAPI server and Vite together via
concurrently; ctrl+c stops both. Vite proxies /api/* and /ws/* to
FastAPI, so only one URL matters in the browser. There's no build step —
production still loads static/ as-is from FastAPI on :8765.
Periscope can run as a native .app — its own Dock icon and Cmd-Tab
entry instead of a browser tab. The app is a thin Tauri
shell that loads the dashboard from http://127.0.0.1:8765, so the
server still has to be running (uv run server.py, or the launchd
service via bin/periscope install).
Download the latest .dmg from
Releases, or build it
from source:
cd src-tauri
cargo tauri build # needs the Rust toolchain + `cargo tauri`
open target/release/bundle/macos/Periscope.appRelease .dmgs are unsigned, so Gatekeeper blocks them on first
launch. After dragging Periscope.app into /Applications:
xattr -dr com.apple.quarantine /Applications/Periscope.appThe ✨ button on each session header asks Haiku 4.5 to suggest fresh, descriptive names for every window in the session based on current pane content. Requires an Anthropic API key:
cp .env.example .env
# then edit .env and paste your keyPeriscope can push messages into the Claude Code sessions it spawns and surface Claude's replies in its UI. This uses Claude Code's channels feature, currently in research preview.
Claude Code keeps user-level MCP servers under the mcpServers key in
~/.claude.json. Merge the periscope entry in (don't overwrite the
file — it holds lots of other Claude Code state):
jq '.mcpServers.periscope = {
"command": "uv",
"args": ["run", "--script", "/ABSOLUTE/PATH/TO/periscope/channel_shim.py"]
}' ~/.claude.json > ~/.claude.json.tmp && mv ~/.claude.json.tmp ~/.claude.jsonReplace /ABSOLUTE/PATH/TO/periscope/ with your local checkout path.
After this, restart any running claude sessions you want channels in
(it's read at invocation time) or spawn fresh ones via periscope's
+ claude button.
When you click + claude in periscope, the spawned command is
claude --dangerously-load-development-channels server:periscope. Claude
launches channel_shim.py as a stdio child. The shim proxies MCP
messages over a unix socket (/tmp/periscope-mcp.sock) to periscope's
in-process MCP server, and reconnects transparently across periscope
restarts.
The --dangerously-load-development-channels flag is required because
channels are in research preview — bare --channels only resolves
allowlisted entries.
- Push to Claude: open a pane's modal, type a message in the
composer in the Messages section, and submit. Claude sees it on its
next turn as a
<channel source="periscope">block. - Replies from Claude: Claude can call the
replytool withkind="need_human",kind="done", orkind="info"(the default). Messages show in the modal's Messages section;need_humantriggers a pulsing red border on the pane card and fades the rest of the grid.
If a session was started outside periscope (no dev-channels flag), the push composer is disabled with a tooltip.
MIT — see LICENSE.