agentpeek is the free, self-hosted browser control panel for Claude Code. Start a session on your Linux/WSL2 box, drive it as a terminal or a chat, and reach it from any browser over your tailnet โ it keeps working when you close the laptop.
You start Claude Code (or any shell) in a session, close the laptop, and it
keeps working. Open the same browser tab from another machine and you're right
back in it โ the session never stopped. Sessions are plain named tmux
sessions, so the exact same ones are reachable over SSH with tmux attach
.
agentpeek is where the work happens: you drive the AI agents that generate your content โ docs, code, reports, sites. Its companion ** filepeek** is the downstream viewer that renders the
.md
/.html
/.xlsx
files those agents leave behind. See Companion tool.
Browser โโโบ uvicorn/FastAPI :8090 (127.0.0.1)
โโโ / static frontend (sidebar + iframe)
โโโ /api/... session lifecycle REST API โ tmux (app/multiplexer.py)
โโโ /term/... reverse proxy (HTTP + WebSocket) โ ttyd :7681
ttyd runs bin/agentpeek-attach <name>
= replay history + tmux attach
Everything binds to 127.0.0.1
and is served from one port, so exposing it on
the tailnet is a single command (see below). The terminal iframe is
same-origin, which keeps clipboard access and tailscale serve
simple.
app/multiplexer.py
โ the only module that knows about tmux (swap point for another multiplexer).bin/agentpeek-attach
โ ttyd entrypoint; validates the session name, replays pane history (tmux capture-pane -S -
) so browser scrollback covers output from before the attach, thenexec tmux attach
.- Switching sessions in the sidebar swaps the iframe URL; the old iframe's
WebSocket closes, ttyd reaps that
tmux attach
client, and tmux detaches it cleanly โ the browser is never attached to two sessions.
On a fresh Debian/Ubuntu (incl. WSL2), setup.sh
installs everything it needs
for you โ the OS packages (tmux
, curl
, python3-venv
) via sudo apt
, plus
ttyd
, Claude Code, and the Python deps โ so it's one command. One thing it can't do for you:
systemd must be enabledโ the services run under it. On WSL2, add this to/etc/wsl.conf
, then runwsl --shutdown
in Windows (PowerShell) and reopen your terminal:Ubuntu 24.04 on recent WSL enables systemd by default;
[boot]
systemd=true
setup.sh
checks and tells you if it's missing.
Claude Code is installed via the official native installer (a standalone binary
in ~/.local/bin
, no Node/npm). After setup, sign in from agentpeek's Claude
chip (or run claude
once) โ needed for UI (chat) mode; shell mode needs no sign-in.
Also: you need git
to clone the repo and Python 3.10+ (already on Ubuntu
22.04/24.04); the bundled ttyd
binary is x86_64. For remote/mobile access
you need Tailscale โ setup.sh
offers to install it for you (or set
AGENTPEEK_INSTALL_TAILSCALE=1
/0
to skip the prompt).
โ the downstream viewer that renders thefilepeek(recommended companion).md
/.html
/.xlsx
/code files your agents generate, so you can browse them in the browser.setup.sh
offers to clone and install it next to agentpeek (or setAGENTPEEK_INSTALL_FILEPEEK=1
/0
to skip the prompt). SeeCompanion tool.
Clone into your Linux home, not a Windows drive โ a repo under /mnt/c/...
is slow and lacks real Unix permissions, so setup.sh
refuses to run there.
cd ~ && git clone https://github.com/thrinz/agentpeek.git && cd agentpeek
./setup.sh
It installs the apt prerequisites (tmux
/curl
/python3-venv
) and ttyd
if
missing, installs Claude Code, creates .venv
and installs the Python deps
(including claude-agent-sdk
), sources conf/agentpeek.tmux.conf
from
~/.tmux.conf
, installs and enables three systemd user services (agentpeek-tmux
,
agentpeek-ttyd
, agentpeek
), and enables linger so everything starts at WSL2
boot. At the end it prompts you to set a browser login password (hashed into
~/.config/agentpeek/agentpeek.env
) and prints the URL โ http://localhost:8090
. It's idempotent โ safe to re-run โ and offers to install Tailscale if it isn't already present.
Custom ports. agentpeek uses 8090
(web) and 7681
(ttyd) by default. To run
it alongside another service โ or another WSL distro that already holds those ports
on a shared localhost
โ pass different ports at setup:
AGENTPEEK_PORT=9090 AGENTPEEK_TTYD_PORT=9091 ./setup.sh
setup bakes them into the systemd units (the app's reverse proxy follows the ttyd port automatically), so re-running with the same values is all it takes.
Headless / cloud (Linode, DigitalOcean, โฆ). setup.sh
runs non-interactively when there's no TTY โ every prompt has an env override, so nothing blocks:
AGENTPEEK_INSTALL_TAILSCALE=1 ./setup.sh
For a one-shot deploy, paste deploy/cloud-init.sh into a Linode/DO droplet's
User Data field โ it clones, installs, joins your tailnet (with a Tailscale auth key), and serves agentpeek over HTTPS. See
for the StackScript and the Linode / DigitalOcean Marketplace submission steps.
deploy/MARKETPLACE.md
Passwordโ with no TTY and none set, setup** generates a strong temporary one**, prints it, and saves it to~/.config/agentpeek/initial-password.txt
(0600). It'sone-time: your first login is forced through achange passwordscreen, after which the temp password and that file are discarded. Override withAGENTPEEK_PASSWORD=โฆ
(final, no forced change), orAGENTPEEK_NO_PASSWORD=1
to rely on tailnet ACLs only.Claude sign-inโ not done at setup (no host browser). After install, sign in from the** Claude chip**in the web UI, orclaude auth login
over SSH.Reach it over Tailscale onlyโ agentpeek binds127.0.0.1
; never expose8090
/7681
on the public interface.tailscale serve
is the intended door.- Cloud VMs are often
rootโ setup setsIS_SANDBOX=1
so UI mode andcds
work (the agent then acts as root without per-tool prompts).
Expose on the tailnet (HTTPS, tailnet-only โ never the public internet):
tailscale serve --bg --https=9443 http://127.0.0.1:8090
The app is the same on macOS; only the installer differs (Homebrew + launchd
instead of apt + systemd). Use setup-macos.sh
rather than setup.sh
:
git clone https://github.com/thrinz/agentpeek.git && cd agentpeek
./setup-macos.sh
It brew install
s tmux
and ttyd
, installs Claude Code, creates the venv, and renders the templates in launchd/ into
~/Library/LaunchAgents
(dev.agentpeek.tmux
, dev.agentpeek.ttyd
, dev.agentpeek.app
). The agents
start at login (launchd RunAtLoad
, no linger needed). Same port overrides apply
(AGENTPEEK_PORT=โฆ AGENTPEEK_TTYD_PORT=โฆ ./setup-macos.sh
). Manage it with:
launchctl kickstart -k gui/$(id -u)/dev.agentpeek.app # restart
launchctl print gui/$(id -u)/dev.agentpeek.app # status
tail -f ~/Library/Logs/agentpeek/app.log # live logs
launchctl bootout gui/$(id -u)/dev.agentpeek.app # stop
Tailscale on macOS installs from the App Store / standalone app,
then tailscale serve
works exactly as above.
A prebuilt multi-arch image (linux/amd64
linux/arm64
) is published to GitHub Container Registry, so you can run agentpeek anywhere Docker runs โ Linux, macOS, or Windows (the container is Linux; Docker Desktop runs it on Mac/Windows). This is also the only way to run it on Windows, which can't host it natively.
docker pull ghcr.io/thrinz/agentpeek:latest
The quickest start is docker-compose.yml:
PROJECTS_DIR=~/projects docker compose up -d
PROJECTS_DIR=~/projects ANTHROPIC_API_KEY=sk-ant-... docker compose up -d
It mounts your projects
tree (the agent's working files), and named volumes for
the app state (~/.config/agentpeek
) and Claude auth (~/.claude
) so they survive
restarts. UI (chat) mode needs Claude signed in, but the API key is optional โ
leave it out and sign in with your Pro/Max subscription from the in-app Claude
chip (it persists in the ~/.claude
volume), mount your host ~/.claude
to reuse an
existing login, or pass ANTHROPIC_API_KEY
for per-token billing.
Exposure:the compose file bindsโ agentpeek runs an autonomous agent, so don't publish it on the open internet. Reach it over your tailnet:127.0.0.1
on purposetailscale serve --bg --https=9443 http://127.0.0.1:8090
.
The image is built and pushed by .github/workflows/docker.yml on every push to
main
and every v*
tag (no cloud credentials โ it uses the
built-in GITHUB_TOKEN
).The sidebar groups sessions into collapsible folders you define yourself
(create them with the folder button; up to two nested levels). A General
folder always exists as the catch-all โ sessions created from the CLI land
there. The folder is stored on the tmux session itself as the
@agentpeek_group
session option, so it survives renames; the folder list
lives in ~/.config/agentpeek/folders.json
.
The create dialog asks for: a folder (required), a Type โ Shell (a tmux
terminal) or UI (a Claude chat โ see UI mode) โ a
working directory picked from a collapsible tree rooted at ~/projects
(DIRS_ROOT
in app/main.py
), and for Shell type a start option (AI types your launcher command into the new shell, or plain Shell).
AI start optiontypescds
into the new shell.setup.sh
installscds
to~/.local/bin
asclaude --dangerously-skip-permissions
(edit that file, or thesend-keys
command inapp/multiplexer.py
, to launch a different agent likeaider
). It won't overwrite an existingcds
, so your customisation sticks.
Running as root?Claude Code refuses to skip permissions as root, so UI mode andcds
fail with exit 1.setup.sh
detects root and setsIS_SANDBOX=1
in the env file (andcds
sets it too) to allow it โ this lets the agent act as root without per-tool prompts, so prefer running agentpeek as a normal user.
For an AI (Shell + AI) session the create dialog also offers โNotify my phone when Claude finishesโ with a dropdown of remembered ntfy topics โ see below.
Get a push on your phone every time Claude finishes a turn in a session, using the free ntfy.sh service. Three pieces cooperate:
A Claude Code Stop hook(one-time, per machine). Add this to~/.claude/settings.json
โ it pushes only whenCLAUDE_NOTIFY_TOPIC
is set, so sessions without a topic stay silent:
{
"hooks": {
"Stop": [
{ "hooks": [
{ "type": "command",
"command": "[ -n \"$CLAUDE_NOTIFY_TOPIC\" ] && curl -s -H \"Title: Claude Code\" -d \"Claude finished a turn in $(basename \"${CLAUDE_PROJECT_DIR:-session}\")\" \"https://ntfy.sh/$CLAUDE_NOTIFY_TOPIC\" >/dev/null 2>&1 || true",
"async": true }
] }
]
}
}
setscds
CLAUDE_NOTIFY_TOPIC
from its first argument โcds
(off),cds on
(uses$CDS_NOTIFY_TOPIC
, export your default in your shell rc), orcds <topic>
(a custom topic). The installedbin/cds
already does this. -
The create dialog ties it together: tickNotify my phone, pick or add a topic, and agentpeek typescds <topic>
into the new session for you. Topics you add are remembered in the browser for next time.
On your phone, install the ntfy app (iOS/Android) and subscribe to the same
topic name. Topics are public unguessable strings โ anyone who knows the name
can read its messages, so use something random like claude-$(openssl rand -hex 4)
and don't share it.
On phones/tablets a touch key bar appears under the terminal when a Shell
session is attached, with the keys a soft keyboard can't reliably send to a
terminal: esc, tab, ctrl (sticky โ tap it then a letter for Ctrl-combos),
arrow keys, scroll up/down, and paste. Every button routes through
tmux send-keys
on the server (POST /api/sessions/<name>/keys
), so it works
even where iOS Safari's keyboard events don't reach xterm. Pair it with
tailscale serve --https
so the clipboard-based paste has a secure context.
A UI session is a chat with the Claude Agent SDK running in the chosen
directory โ the same agent as a terminal Claude Code session (it edits files,
runs bash, uses tools), rendered as a chat instead of a terminal. It needs the
claude
CLI installed and a Claude connection.
Streaming token-by-token with a live cursor;markdown + GFM tables + code rendering with copy-code buttons; tool calls and results shown inline (collapsible).Message queueโ keep typing while the agent works; messages run in order.** Stopbutton or Esc**interrupts the current turn.in the input autocompletes files in the working directory to reference.@
Model picker(Opus / Sonnet / Haiku) per session, switchable live; per-turn and running** costshown in the header; optional voicein (mic โ speech-to-text) and out (speak replies). Always-on background agents**โ the agent keeps running if you close the browser; reconnecting (auto-reconnect on drop) reloads the transcript and the conversation resumes (SDKresume
). Idle agents (no browser, not working) are suspended after 30 min and resume on the next message. Sessions run autonomously (bypassPermissions
) โ same trust model as your terminal.
Registry + transcripts live under ~/.config/agentpeek/
(ui_sessions.json
,
ui_transcripts/
).
UI-mode sessions run the Claude Agent SDK, which authenticates with
whatever the host's claude
CLI uses. The Claude chip at the bottom of the sidebar shows the connection state and, when not connected, opens a sign-in panel:
Sign in with Claude drivesclaude setup-token
server-side: it prints an authorize URL (shown in the panel), you approve it in your browser and paste the code back, and the resulting long-lived subscription token is saved asCLAUDE_CODE_OAUTH_TOKEN
in~/.config/agentpeek/agentpeek.env
โ the same flow as/login
in the terminal. (Requires a Claude Pro/Max/Team/Enterprise plan.)Use an API key saves anANTHROPIC_API_KEY
instead (bills per-token).
If the host is already logged into Claude Code (~/.claude/.credentials.json
exists), the chip shows connected and nothing else is needed. Disconnecting clears only agentpeek's saved token/key โ it never touches the host's own login.
UI mode runs Claude Code, which can route to Amazon Bedrock or Google
Vertex AI instead of the first-party Anthropic API. agentpeek detects this and
adapts: the chip shows "Using AWS Bedrock / Google Vertex", the in-app sign-in
panel is hidden (the cloud provider authenticates you), the per-turn cost is
hidden (billing goes through the cloud account), and the model picker is
hidden โ on these backends the opus
/sonnet
/haiku
aliases don't resolve, so agentpeek lets Claude Code use the model you set in the environment.
There's no UI for this yet โ configure it by hand in
~/.config/agentpeek/agentpeek.env
(loaded by the systemd unit; for the manual
uvicorn
run, the vars must be in the process environment). Set the backend flag, the model IDs, and the provider credentials, then restart agentpeek.
Amazon Bedrock:
CLAUDE_CODE_USE_BEDROCK=1
AWS_REGION=us-east-1
AWS_PROFILE=default
ANTHROPIC_MODEL=us.anthropic.claude-opus-4-...-v1:0
ANTHROPIC_SMALL_FAST_MODEL=us.anthropic.claude-haiku-4-...-v1:0
Google Vertex AI:
CLAUDE_CODE_USE_VERTEX=1
CLOUD_ML_REGION=us-east5
ANTHROPIC_VERTEX_PROJECT_ID=your-gcp-project
ANTHROPIC_MODEL=claude-opus-4-...
ANTHROPIC_SMALL_FAST_MODEL=claude-haiku-4-...
Look up the exact model IDs available in your account/region from the provider console (they change over time). The agent's tools (bash, file edits, etc.) run locally on your box regardless of which backend serves the model. Note that Anthropic's server-hosted features (Managed Agents, server-side tools) aren't available on Bedrock/Vertex โ but agentpeek uses the local Claude Code agent, so that doesn't affect it.
agentpeek binds to 127.0.0.1
only and is reachable remotely solely through
Tailscale (tailscale serve
), so the baseline access control is tailnet membership/ACLs. Do not port-forward 8090/7681 to any public interface.
Optional key-based login (same scheme as filepeek) adds a second factor on top of the tailnet. The easiest way to enable it is to run ./setup.sh โ it prompts for a password, hashes it, writes
~/.config/agentpeek/agentpeek.env
,
and restarts the service. (Re-run it any time to change the password, or set
AGENTPEEK_PASSWORD=โฆ ./setup.sh
to do it non-interactively.)To wire it up by hand instead, set either env var in
~/.config/agentpeek/agentpeek.env
(read by the systemd unit):
.venv/bin/python -m app hash-password
AGENTPEEK_PASSWORD_HASH=pbkdf2_sha256$200000$.... # browser login
AGENTPEEK_TOKEN=<random-string> # scripted access: Authorization: Bearer <token>
AGENTPEEK_SECRET=<random-string> # keeps sessions valid across restarts
With auth on, the browser logs in at /login
(PBKDF2-checked password โ signed, HttpOnly session cookie, 7-day TTL, per-IP lockout after repeated failures) and the terminal WebSocket is gated on the same cookie. With neither var set, the app is open โ the accepted posture for a purely local, single-operator setup. Turning it on is recommended once UI mode can drive agents that edit files over the tailnet.
Each session's โฎ menu has Copy SSH attach command, which copies a ready one-liner for that exact session:
ssh -t <user>@<tailscale-hostname> tmux attach -t <session-name>
(-t
forces a TTY so tmux runs interactively.) There is also Copy local
attach command (tmux attach -t <name>
) for when you are already on the host.
history-limit 1000000
โ "unlimited" scrollback, bounded by memory (applies to sessions created after the setting is loaded).terminal-overrides ',xterm*:smcup@:rmcup@'
+status off
โ disables the alternate screen and the tmux status bar so output flows into the browser's native scrollback buffer and wheel-scrolling/select-to-copy work without tmux copy-mode. Side effects: SSH attaches also get no status bar, and the screen isn't restored after detaching in a CLI terminal. Both accepted for this tool.mouse off
โ xterm.js owns selection (select โ Ctrl/Cmd+C copies natively).
- The tmux server runs on a
dedicated socket(
tmux -L agentpeek
) owned byagentpeek-tmux.service
, in its own cgroup. So restarting or crashing the app (agentpeek
/agentpeek-ttyd
)never touches your sessionsโ selected viaAGENTPEEK_TMUX_SOCKET=agentpeek
(honored byapp/multiplexer.py
andbin/agentpeek-attach
; same socket in the launchd and Docker setups). Stopping It usesagentpeek-tmux
is the only thing that kills the tmux server and all sessions.exit-empty off
, so it also stays up with zero sessions (no placeholder session needed). To reach it by hand:tmux -L agentpeek ls
.Terminal sessions are restored after the tmux server dies(a host reboot, or a Docker container restart โ where the server lives inside the app container). agentpeek keeps a manifest at~/.config/agentpeek/shell_sessions.json
(name, dir, group, and for AI sessions a Claude session id) and recreates the sessions on startup. AI sessions arelazily resumed: each is recreated empty with a pendingcds --resume <id>
thatbin/agentpeek-attach
runs the first time you open it, so a restart never spawns every Claude at once. Resume needs Claude's transcripts (~/.claude
) โ already a persisted volume in Docker.- Logs:
journalctl --user -u agentpeek -u agentpeek-ttyd -f
. - The sidebar polls every 3 s and on tab focus, so CLI-created sessions appear automatically.
agentpeek is the primary tool โ it's where AI agents generate your content. ** filepeek** is its downstream companion; together they cover the loop of working with AI agents on a remote/WSL2 box:
agentpeekโrunthe agents that generate the content: persistent browser terminal sessions.filepeekโviewwhat they produced: renders the Markdown, HTML, Office, and code files those agents generate, in the browser.
Both are self-hosted, single-operator tools that bind to 127.0.0.1
and go over Tailscale, and they share a design language.
Install filepeek. setup.sh
offers to clone and install it next to agentpeek
(answer the Install filepeek now? prompt, or set AGENTPEEK_INSTALL_FILEPEEK=1
to do it non-interactively). To do it by hand instead:
cd ~ && git clone https://github.com/thrinz/filepeek && cd filepeek
./install.sh
FILEPEEK_ROOT=~/projects .venv/bin/python app.py
tailscale serve --bg --https=9444 http://127.0.0.1:8765