Report a vulnerability privately through a GitHub Security Advisory. Do not report it in a public GitHub issue, discussion, or pull request.
Include these in the report:
- Detailed steps that reproduce the issue.
- The version or commit hash where you found it.
- Mitigations or workarounds, if you know any.
What happens next:
- We acknowledge your report within 14 business days.
- We work with you to understand and confirm the issue.
- We fix the issue and coordinate the release with you.
- Allow 90 days for a fix before you disclose the issue publicly.
- After a patch is released, allow 30 more days for users to update before you disclose it. The total is 120 days at most.
Security fixes go into the next release. The npm, Homebrew, and GitHub release channels are built from the same tag.
| Version | Supported |
|---|---|
| Latest commit on the default branch | Yes |
Latest npm release (@gbasin/agentboard and its platform packages) |
Yes |
Latest Homebrew formula (gbasin/tap/agentboard) |
Yes |
| Latest binaries attached to a GitHub release | Yes |
| All other versions | No |
Agentboard is a web interface to your tmux server. A client that can connect to it can do anything that you can do in a terminal, as your user. The sections below describe who can connect and what a connection allows.
Agentboard has no login, no tokens, and no user accounts. The server does not check who a client is. Network reachability is the only access control.
| Listener | When it exists | Protocol |
|---|---|---|
HOSTNAME:PORT |
Always. The default is 127.0.0.1:4040. |
HTTPS if TLS_CERT and TLS_KEY are set, else HTTP. |
Tailscale IPv4 address:PORT |
HOSTNAME is exactly 127.0.0.1, and tailscale ip -4 returns an address at startup (5-second timeout for each location it tries). |
Same as the main listener. |
127.0.0.1:random port |
The terminal relay is on (the default; AGENTBOARD_TERMINAL_RELAY=0 turns it off). |
Always HTTP. |
HOSTNAME. The server binds to the address inHOSTNAME.0.0.0.0listens on all interfaces, including your LAN. AHOSTNAMEthat equals the machine hostname is ignored, because containers and some CI images export it automatically. The server then binds127.0.0.1..envfiles. Bun, the runtime, loads.env,.env.<NODE_ENV>,.env.local, and.env.<NODE_ENV>.localfrom the directory where Agentboard starts. This includes the Homebrew and npm binaries. AHOSTNAME,PORT, orALLOW_KILL_EXTERNALin such a file applies without any message. If you startagentboardin a project whose.envsetsHOSTNAME=0.0.0.0, the board listens on all interfaces. The launchd release service starts in your home directory, so~/.envand~/.env.localapply to it, and the installer's checks onHOSTNAMEand TLS do not see them.- Tailscale. The server runs
tailscalefromPATH, then/Applications/Tailscale.app/Contents/MacOS/Tailscale. It looks once, at startup. It does not add this listener for any otherHOSTNAME, includinglocalhost. With0.0.0.0, the main listener already covers the Tailscale interface. Every device that your tailnet access rules let reach this machine can use the board. - Internal listener. With the relay on, the public listener runs in a worker, and the main process serves HTTP on a random loopback port. That port accepts the full HTTP API and
/ws, with no TLS. Any local user or process on the machine can reach it, as it can reach the main listener on127.0.0.1. - Development server.
bun run devstarts Vite with--host, so it listens on all interfaces on port 5173. Vite forwards/apiand/wsto the backend. This exposes the backend to your network whateverHOSTNAMEsays.
TLS is opt-in. Set both TLS_CERT and TLS_KEY to serve HTTPS on the main and Tailscale listeners. The production server does not look for certificate files on its own. See PWA installation for a working setup.
WebSocket (/ws). The server accepts these client message types.
-
Terminal I/O:
terminal-attach,terminal-detach,terminal-input,terminal-paste,terminal-resize,terminal-resume,terminal-ack,tmux-cancel-copy-mode,tmux-check-copy-mode. A client can read and type into every session that the board lists. The other tmux windows thatterminal-attachaccepts depend on the listener:- With the terminal relay on, which is the default, the main and Tailscale listeners accept only a listed session. A
tmuxTargetin the message must equal the window that the board recorded for that session. - The internal loopback listener, and the main listener when
AGENTBOARD_TERMINAL_RELAY=0, accept the ID of a listed session together with anytmuxTargetthat has valid syntax. A client there can attach to any window on your tmux server, including windows thatDISCOVER_PREFIXEShides.
These checks are not an access control. A client can create a session that runs any command, including
tmux, so it can reach every window on the server.DISCOVER_PREFIXESonly filters the list. - With the terminal relay on, which is the default, the main and Tailscale listeners accept only a listed session. A
-
Session control:
session-create(any project directory and any command, run as your user),session-kill,session-rename,session-refresh,session-wake(runs the agent's resume command),session-hibernate,session-move-to-history. -
Keepalive:
ping.
Limits on these messages:
- Discovered sessions. Windows from your other tmux sessions appear on the board. A client can attach to them, type into them, and rename them. It can kill them only when
ALLOW_KILL_EXTERNAL=true. - Remote hosts. For sessions on remote hosts,
AGENTBOARD_REMOTE_ALLOW_ATTACH=trueallows attach and input.AGENTBOARD_REMOTE_ALLOW_CONTROL=trueallows create, kill, and rename, and it also turns on attach unlessAGENTBOARD_REMOTE_ALLOW_ATTACHis set. Both default tofalse. Remote create accepts only hosts listed inAGENTBOARD_REMOTE_HOSTS. Remote kill and rename apply only to windows in the remote tmux session namedTMUX_SESSION, whoever created them. - Kill rate limit.
session-killis limited to 3 kills in 5 seconds for each connection. This stops an accidental cascade from a held shortcut. It is not a security control: a new connection resets it, andsession-hibernateand a library archive also stop sessions, with no limit.
HTTP. The server has these API routes, plus the static frontend files.
| Group | Routes | What they allow |
|---|---|---|
| Uploads | POST /api/paste-file, POST /api/paste-image |
Write a file to the host. paste-file accepts any type up to 40 MB and writes it to a new agentboard-upload-* directory (mode 0700) in the system temporary directory, with file mode 0600. paste-image accepts PNG, JPEG, GIF, and WebP up to AGENTBOARD_PASTE_IMAGE_MAX_BYTES (default 40 MB) and writes /tmp/paste-<time>-<random>.<ext> with the default file mode, which is world-readable under the usual umask. Agentboard does not delete uploaded files. |
| Directory browsing | GET /api/directories |
List the subdirectory names of any directory that your user can read. Up to 200 entries per request. File names are not listed, but the response shows whether a given path exists, is a file, or is not readable. |
| Transcripts | GET /api/session-preview/:id, GET /api/session-media/:id, .../items, .../neighbors |
Read any session's agent transcript, including History sessions, 500 lines per request with paging through the whole file, and images, documents, and links from it. Media reads are limited to the session's own transcript and its subagent transcripts. |
| Host clipboard (macOS) | GET /api/clipboard-file-path |
Read the host clipboard. Returns a copied file's path, or writes a copied image to a temporary PNG and returns its path. |
| Session library | GET /api/library, GET /api/library/:id, GET /api/library/health, PUT /api/library/settings, POST /api/library/:id/resume, PATCH /api/library/:id |
Search saved sessions, resume one (runs its resume command), rename, pin, archive, or hibernate one. Archive and hibernate stop the session's tmux window. |
| Settings | GET and PUT for /api/settings/tmux-mouse-mode, terminal-colors, prefer-window-name, synced, history-max-age-hours, inactive-max-age-hours |
Change server-wide settings. tmux-mouse-mode sets the tmux mouse option on the managed session and its grouped sessions. |
| GitHub | POST /api/pr-info, GET /api/pr-checks |
Run gh with your credentials to read the state, title, author, and checks of up to 50 pull request URLs per request, including private ones that you can see. |
| Status | GET /api/health, GET /api/sessions, GET /api/server-info |
Read the session list, the port, the Tailscale address, and the protocol. |
| Client log | POST /api/client-log |
Write an event to the server log. |
The server checks the Origin header to stop other web sites from using the board through your browser.
The rule. The check applies to the /ws WebSocket handshake and to every request whose method is not GET, HEAD, or OPTIONS, on every path. On those requests:
- A request without an
Originheader is accepted. Browsers sendOriginon WebSocket handshakes and on cross-site writes. curl, scripts, and other non-browser clients usually do not. - A request is accepted if the host and port in
Originequal theHostheader. A missing port means the default port of theOriginscheme. When Agentboard serves TLS itself (TLS_CERTandTLS_KEYare set), theOriginmust also usehttps. A server that serves plain HTTP accepts both schemes, because a proxy that terminates TLS, such as Tailscale Serve, sends anhttpsorigin to an HTTP server. - A request is accepted if its origin is listed in
AGENTBOARD_ALLOWED_ORIGINS. An entry matches exactly, scheme included. Use this when a reverse proxy rewritesHost. - Every other request gets 403. This includes
Origin: nulland a malformedOrigin.
The check applies to the main listener, the Tailscale listener, and the internal loopback listener.
This is not authentication. A client that sends no Origin, or sends a matching one, passes. Every program that can reach a listener still has full access.
Gap 1: DNS rebinding. A hostile page can load from a name that the attacker controls, then make that name resolve to 127.0.0.1 or to your Tailscale address. The browser then sends Origin and Host with the same attacker name, so the rule accepts the request. The page can then open /ws, send writes, and read responses as a same-origin page. The server does not check the Host header against a list of names that it expects.
Gap 2: the scheme behind a TLS proxy. When a proxy terminates TLS, Agentboard serves plain HTTP and cannot tell which scheme the browser used. An http origin whose host and port equal the Host header therefore passes. An attacker who can answer plain HTTP requests for that host name, for example on a network that the browser's traffic crosses, can serve a page from that origin and use the board through it. When Agentboard serves TLS itself, rule 2 closes this gap.
Gap 3: cross-site GET requests. GET requests are not checked. A page on another site can make the browser send them, for example with an image or a link. The server sends no CORS headers, so that page cannot read the responses. Side effects still happen. On macOS, GET /api/clipboard-file-path reads the host clipboard and can write a copied image to a temporary file. GET /api/pr-checks runs gh with your credentials.
- tmux
set-clipboard. Inptyterminal mode, which is the default, Agentboard sets the server optionset-clipboard onfor your whole tmux server. It does not restore the old value. SetAGENTBOARD_TMUX_SET_CLIPBOARD=0to prevent this. - tmux server identity. Agentboard sets the global user option
@agentboard-server-idonce per tmux server. - Grouped sessions. Browser connections create
<TMUX_SESSION>-ws-*sessions in your tmux server. Agentboard removes them when it closes the terminal. After a disconnect, the relay keeps the terminal for up to 60 seconds (AGENTBOARD_RELAY_GRACE_MS) so that the browser can reconnect. Startup prunes the ones that an earlier server left behind. - tmux global environment. At startup, if the global environment of your tmux server contains
AGENTBOARD_STATIC_DIR, Agentboard removes that variable,NODE_ENV, and everynpm_*variable from it. An older Agentboard version that started the tmux server could leave these behind. Panes that already run keep their environment. New windows, including ones that you create outside Agentboard, no longer inherit these variables.
| Data | Location | Mode |
|---|---|---|
| Data directory | ~/.agentboard (AGENTBOARD_DATA_DIR) |
0700. Agentboard sets this on the directory that holds the database. If AGENTBOARD_DB_PATH points elsewhere, the data directory keeps the mode it was created with. |
Database, with -wal and -shm files |
agentboard.db |
0600 |
| Log file | agentboard.log (LOG_FILE) |
Your umask. Usually 0644. |
| Other files (lock, mirrors, recovery data) | In the data directory | Varies. Protected by the directory mode. |
The database holds session names, project paths, launch commands, transcript paths, last prompts, pull request links, session events and errors, settings, and terminal previews when preview capture is on. A launch command can contain a secret if you put one on the command line. The log holds event metadata: session names, project paths, tmux targets, and errors. It also holds the full command line of a remote session that failed to start, and any event name and data that a browser posts to /api/client-log. Terminal input and pasted content are not logged. Kill requests are logged with the client's IP address and user agent. A LOG_FILE outside the data directory is not protected by the directory mode.
- npm. The release workflow (release.yml) publishes the main package and the four platform packages with
npm publish --provenance. It authenticates with GitHub OIDC trusted publishing, not a stored npm token. - Binaries and Homebrew. The same workflow builds the binaries, attaches them to the GitHub release, and writes the Homebrew formula with the SHA-256 of each tarball. The GitHub release assets have no signature or attestation, and the macOS binaries are not signed with a Developer ID or notarized. The npm platform packages contain the same binaries and have npm provenance.
- Actions.
ci.ymlandrelease.ymlpin third-party actions to commit SHAs.release-please.ymlpins its token action to a commit SHA and uses a version tag forgoogleapis/release-please-action. - Dependency scanner.
bun run deps:riskruns scripts/dependency-risk.ts. It combinesbun auditadvisories withbun outdatedversion lag. It fails athighseverity by default. CI runs it with--threshold critical, so CI fails only on critical advisories.
Last updated: November 2026