Claude-Code-Agent-Monitor defaults to loopback because its own env file cites an advisory
🚀 A real-time monitoring dashboard for Claude Code & Codex, built with SQLite3, Node.js, Express, React, Vite, TailwindCSS, & WebSockets. It tracks sessions, agent activity, tool usage, and subagent orchestration, providing live analytics, a Kanban status board, status notifications, a cute buddy, & an interactive web UI/MacOS/Windows native app.
At a glance
- What is it?
- A local dashboard that watches Claude Code, Cursor and Codex sessions through native hooks plus filesystem transcript discovery, storing everything in one SQLite database and broadcasting over WebSockets. The shipped environment example names the security advisory behind its loopback default, says the server can spawn the claude binary, and leaves authentication off unless you turn it on.
- Who is it for?
- Reach for this if you want to see what several agents are doing at once across three tools, and you accept that the monitoring server reads transcripts, holds cost and token data, and can launch the agent binary itself. Three things to check before exposing it anywhere.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 2 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 2, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The shipped environment file cites an advisory and names what the server can do
The most useful paragraph in the repository is a comment in `.env.example`, and it is not a warning about a future risk. It is a reference to an advisory that already exists.
# Interface to bind. SECURITY: defaults to 127.0.0.1 (loopback) so the dashboard
# is NOT reachable from the network out of the box (GHSA-gr74-4xfh-6jw9). The
# server reads transcripts, exports all data, and can spawn `claude`, so binding
# it to the network without auth exposes all of that.So the project enumerates its own exposure surface in three clauses. The server reads transcripts, it exports all data, and it can spawn the `claude` command. The third of those is the one that matters for a deployment, because a monitoring process with the ability to start the agent it monitors is not the same trust level as a read-only viewer.
The advisory identifier is given, which means the loopback default is a mitigation for something that was reported rather than a precaution. The comment also says to set the dashboard token if you widen the bind, which frames the network exposure as a deliberate act.
The compose file takes the same posture independently, publishing the port as `${CCAM_DASHBOARD_BIND:-127.0.0.1}:${DASHBOARD_PORT:-4820}:4820`, so the published interface is loopback unless you set that variable too.
Authentication is off by default and the token is accepted in the URL
The auth comment is equally explicit about the trade:
# Optional auth token. When set, every /api/* request and the WebSocket must
# present it (Authorization: Bearer <token>, x-dashboard-token header, or
# ?token=). Strongly recommended whenever DASHBOARD_HOST is non-loopback. Unset
# by default — the loopback bind is the trust boundary.Three things follow. There is no token unless you create one, and the project describes that as the design rather than as an omission. When you do set one, it gates every API route and the WebSocket, not just the HTML. And the token can arrive three ways.
The third of those is worth pausing on. Accepting the credential as a `?token=` query parameter is convenient for a WebSocket client and for a link you can paste, but a URL is the form most likely to be written to a request log, a proxy log or browser history. The header forms, `Authorization: Bearer` and `x-dashboard-token`, do not have that property. Nothing on the page recommends preferring them, and the three are presented as equals.
The example value suggested is a placeholder, `change-me-to-a-long-random-string`, which at least makes clear that the intended token is long and random rather than a password.
Three tokens, each with a file-backed variant for secrets
The environment example defines three separate credentials rather than one, and the split is deliberate.
`DASHBOARD_TOKEN` gates the API and the WebSocket. `DASHBOARD_HOOK_TOKEN` is an independent token for the two local hook routes, `/api/hooks/event` and `/api/hooks/codex`, and the comment says to keep it separate from the dashboard token when hooks arrive through a remote HTTPS endpoint, while noting that localhost hooks stay zero-config when neither value is set. A third token gates `POST /api/hooks/ingest-batch`, described as the remote-push route a roaming or NAT'd machine uses to push its own session data.
That is a reasonable decomposition. A machine that pushes data should not thereby gain read access to everything the dashboard has collected, and a hook endpoint exposed over HTTPS should not share a credential with the browser UI.
Each of the three has a file-backed alternative for Docker and Kubernetes secrets, and the comment states the precedence: the direct value wins when both are set. The compose file uses the file form, mounting a secrets directory and pointing at `/run/ccam-secrets/dashboard-token` and `/run/ccam-secrets/hook-token`, with the host directory defaulting to `./deployments/secrets` and the mount marked read-only. So the container reads credentials from a mounted file rather than from its environment, which is what lets the compose file avoid embedding secrets.
Three agent home directories are mounted read-only, and the mode is overridable
The container sees your agent configuration and your workspace, and the compose file is careful about how:
volumes:
- ${CLAUDE_HOME:-~/.claude}:/home/node/.claude:${CCAM_AGENT_HOME_MODE:-ro}
- ${CURSOR_HOME:-~/.cursor}:/home/node/.cursor:${CCAM_AGENT_HOME_MODE:-ro}
- ${CODEX_HOME:-~/.codex}:/home/node/.codex:${CCAM_AGENT_HOME_MODE:-ro}
- ${CCAM_WORKSPACE_DIR:-.}:/workspace:${CCAM_WORKSPACE_MODE:-ro}
- ${CCAM_SECRETS_DIR:-./deployments/secrets}:/run/ccam-secrets:roFive mounts, and four of them default to read-only. The agent homes for Claude, Cursor and Codex share one mode variable, and the workspace has its own. So a deployment that needs the dashboard to write back into a workspace can set `CCAM_WORKSPACE_MODE` without also opening up the three agent home directories, which is a distinction worth having.
The rest of the container is locked down as well. The service sets `read_only: true` on the root filesystem and gives `/tmp` a 512 MB tmpfs with mode 1777, so the only writable paths are the two named volumes for data and config, which default to `ccam-data` and `ccam-config`.
Two of the environment values are set to fixed paths rather than being configurable: `DASHBOARD_DATA_DIR: /app/data` and `DASHBOARD_ENV_PATH: /app/config/.env`. And `DASHBOARD_LIVENESS_PROBE: 0` switches off the liveness probe, which is worth knowing before you deploy this behind an orchestrator that expects one.
One SQLite database and a written rule against ever scaling past one replica
The compose file opens with a constraint rather than a service definition:
name: ccam
# The dashboard is intentionally a single writer. CCAM stores its operational
# state in one SQLite database, so never scale agent-monitor above one replica
# or run two active containers against the same data volume.That is a design constraint stated as an instruction, and it is not a limitation discovered later. The storage engine is SQLite, the dashboard broadcasts changes to connected clients over WebSockets, and there is exactly one process holding the write side of that state. There is no replication, no leader election and no shared store.
The image reference is worth a second look for anyone deploying from this file alone. It is `image: ${CCAM_IMAGE:-ccam-dashboard:2.2.4}` with a `build:` block alongside it, where 2.2.4 matches the package version. The default image name is a local tag rather than something on a registry, so a compose command that tries to pull will not find it, and the build context is the repository with a target that defaults to `runtime` out of a multi-stage Dockerfile.
Also note that the `build.dockerfile.target` is itself an override, `CCAM_DOCKER_TARGET`, so the same compose file can produce a different image by changing one variable.
Do not build the image with --ignore-scripts, and the base image is pinned by digest
The Dockerfile's comments document two build hazards that would otherwise surface as confusing failures. Both are about lifecycle scripts.
The first is ordering. The root `postinstall` hook fires during `npm ci`, so the postinstall script and its dependency-free npm launcher must be copied into the image before the install runs, or npm aborts with `MODULE_NOT_FOUND`. The comment explains that the hook self-skips when `client/` is absent, which is the case in that stage, and that copying only those two files keeps the dependency cache layer from being invalidated by unrelated edits.
The second is the one that fails quietly:
> Do NOT use --ignore-scripts: that would also skip better-sqlite3's prebuild fetch and silently drop the native SQLite driver.
The driver is a native module fetched as a prebuilt binary during install. Suppressing scripts produces an image that installs cleanly and then cannot open its database, and nothing in the build output says why. For a project whose entire storage model is one SQLite file, that is the single most consequential line in the build file.
The base image is pinned precisely, as `node:24.19.0-alpine3.24` with a `sha256` digest in an `ARG`. So a rebuild months from now gets the same Node and the same Alpine, and the image also includes Git and OpenSSH for update checks and remote data sources, per the header comment, while the application itself runs as a non-root user.
Selecting Claude Code also switches Cursor on, and one variable cannot turn the watcher off
Cursor support has an odd entry point. The page says Cursor needs no separate setup choice: selecting Claude Code on the splash screen also enables Cursor monitoring. One toggle in one editor's onboarding turns on watching a different tool.
The mechanism underneath is a filesystem watcher plus a poll. A watcher discovers `~/.cursor/chats/*/<session>/meta.json` as soon as `agent` starts, before Cursor has written a transcript, and changes to `prompt_history.json` update the session card, the working state and the conversation as soon as a prompt is submitted. When `~/.cursor/projects/*/agent-transcripts` appears, the dashboard merges it into the pending conversation without duplicating the user turn, enriches native titles, projects, turns and subagents, and then snapshots the main and subagent JSONL so the conversation survives Cursor cleaning up its own history.
That last behaviour is a deliberate workaround for a host that deletes its data, and the source root is overridable with `DASHBOARD_CURSOR_HOME`.
The tuning variable has a trap in it. `DASHBOARD_CURSOR_SYNC_MS` tunes the polling fallback, and setting it to `0` disables only periodic scanning, because live filesystem watching stays enabled. So there is no supported way to turn Cursor monitoring off through that variable, which matters for a machine that has both editors installed and did not mean to enable either.
Cursor also has its own cost table rather than borrowing rates from the other two agents: an editable Cursor Pricing table in Settings covering Cursor Grok 4.6 and 4.5, Composer 2.5, and a published third-party model catalog. So the cost figures on the dashboard are only as accurate as a price list someone maintains by hand.
A private package with publish metadata, three npm trees and a data directory in the file list
The packaging metadata is a study in near-publishes. The package is named `agent-dashboard`, it is marked `"private": true`, and it is versioned `2.2.4`, matching the newest GitHub release. At the same time it declares a `files` array, a `bin` entry mapping `ccam` to `bin/ccam.js`, a `main` of `server/index.js`, a license, a homepage and a funding URL.
So the shape of a published CLI is present while the publish flag says otherwise, and the releases are cut from the repository instead. The `bin` name is `ccam`, and the abbreviated form appears throughout the compose file as `CCAM_` prefixed variables, so there is a command line interface whose name the visible part of the readme does not introduce.
The `files` array is the entry that deserves a second look. It lists `server`, `scripts`, `data`, `mcp`, `statusline`, `README.md` and `LICENSE`. Including a `data` directory in a package's file list means whatever is sitting in that directory at publish time is part of the artefact, and the compose file separately uses `/app/data` as the runtime data directory. The same word names a runtime mount and a distributable path.
The scripts section shows the same duplication in the dependency install. The `setup` script runs three separate installs, the root, then `--prefix client`, then `--prefix vscode-extension`, and only then `npm run mcp:install`. So there are at least three independent npm trees with their own lockfiles, including one for a VS Code extension and a `client/package-lock.json` alongside the root `package-lock.json`, in a repository that does not use npm workspaces.
Editorial conclusion
Reach for this if you want to see what several agents are doing at once across three tools, and you accept that the monitoring server reads transcripts, holds cost and token data, and can launch the agent binary itself. Three things to check before exposing it anywhere. The authentication token is unset by default and the project says so plainly, with the loopback bind described as the trust boundary rather than as a default you should keep. The token is accepted as a query parameter, so if you do enable it on a shared host it will end up in request logs. And the filesystem watcher for Cursor cannot be disabled by the variable that looks like it should, since setting the sync interval to zero turns off only periodic scanning. For deployment, the storage model rules out scaling: one SQLite database, one writer, and a compose file that says so in a comment.
Frequently asked questions
Does Claude-Code-Agent-Monitor require authentication?
Not by default. DASHBOARD_TOKEN is unset by default and the shipped environment example says the loopback bind is the trust boundary. When the token is set it must be presented on every API request and the WebSocket, as an Authorization bearer token, an x-dashboard-token header, or a token query parameter.
Why does Claude-Code-Agent-Monitor bind to 127.0.0.1?
The environment example cites security advisory GHSA-gr74-4xfh-6jw9 and states that the server reads transcripts, exports all data, and can spawn the claude command. The compose file publishes the port as ${CCAM_DASHBOARD_BIND:-127.0.0.1}:${DASHBOARD_PORT:-4820}:4820, so the interface stays loopback unless you change it.
Can Claude-Code-Agent-Monitor be scaled to multiple replicas?
No. The compose file states that the dashboard is intentionally a single writer because it stores operational state in one SQLite database, and instructs you never to scale above one replica or run two active containers against the same data volume.
How do I add Cursor monitoring to Claude-Code-Agent-Monitor?
There is no separate switch. Selecting Claude Code on the splash screen also enables Cursor monitoring, through a filesystem watcher on ~/.cursor/chats/*/<session>/meta.json plus a poll tuned by DASHBOARD_CURSOR_SYNC_MS. Setting that to 0 disables only periodic scanning, because live filesystem watching stays enabled.
What secret files does the Claude-Code-Agent-Monitor container expect?
A mounted secrets directory, defaulting to ./deployments/secrets and mounted read-only at /run/ccam-secrets, holding dashboard-token and hook-token. A third token gates POST /api/hooks/ingest-batch, the remote-push route. When a file path and a direct environment value are both set, the direct value wins.
Why should the Claude-Code-Agent-Monitor image not be built with npm ignore-scripts?
The Dockerfile says explicitly not to, because suppressing lifecycle scripts also skips better-sqlite3's prebuild fetch and silently drops the native SQLite driver. It also notes the postinstall hook must be copied before npm ci runs or npm aborts with MODULE_NOT_FOUND.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/hoangsonww-claude-code-agent-monitor)