Run in containers
Run the collector in Docker, in dev sandboxes like Daytona, devcontainers or Codespaces, and in Kubernetes.
tallygraph collect reads the agent history under $HOME. In a container it has to run where that history lives: in the same container as the agent, or in another one with the history directory mounted. Login is not needed: the token comes from an environment variable.
Requirements
- Node.js 22.13 or newer in the image. The official
node:22-slimimage works. - A write token from the Connectors page, passed as
TALLYGRAPH_WRITE_TOKEN. - Outbound HTTPS to
www.tallygraph.dev, or to the URL you set inTALLYGRAPH_ENDPOINT. - The agent history inside the container, at the default path under
HOME(for Claude Code,~/.claude/projects). The full list of paths is in Quickstart.
Do not use tallygraph service install in a container. It registers the job with launchd, systemd or Task Scheduler, and a container usually has none of them. Use collect --watch or a scheduled job instead.
Docker
Check discovery first
Mount the history read-only and run a dry run. It reads and validates, and sends nothing. No token needed.
docker run --rm -u node -e HOME=/home/node \ -v /path/to/.claude/projects:/home/node/.claude/projects:ro \ node:22-slim npx --yes tallygraph collect --dry-run
[tallygraph] discovered: claude (cost basis: estimate) [tallygraph] file: discover — 1 valid, 0 invalid, 0.4 KiB [tallygraph] dry-run: nothing sent, spool untouched
Build a collector image
Installing the CLI in the image avoids a download on every start. The .tallygraph directory is created as the node user so a volume mounted there is writable.
FROM node:22-slim
ARG TALLYGRAPH_VERSION=0.5.0
RUN npm i -g tallygraph@${TALLYGRAPH_VERSION}
USER node
RUN mkdir -p /home/node/.tallygraph
ENTRYPOINT ["tallygraph"]
CMD ["collect"]docker build -t tallygraph-collector . export TALLYGRAPH_WRITE_TOKEN=tg_xxxxxxxx docker run --rm -e TALLYGRAPH_WRITE_TOKEN \ -v /path/to/.claude/projects:/home/node/.claude/projects:ro \ -v tallygraph-state:/home/node/.tallygraph \ tallygraph-collector collect
The tallygraph-state volume keeps the spool between runs, so rows that failed to send go out on the next one. To keep it running, pass collect --watch instead of collect. It stops cleanly on docker stop.
Dev sandboxes: Daytona, devcontainers, Codespaces
Here the agent runs inside the sandbox, so its history is already in the sandbox home. Run the collector there too.
- Install it in the image or setup script with
npm i -g tallygraph, or call it withnpx --yes tallygraph. - Set
TALLYGRAPH_WRITE_TOKENas a secret or environment variable of the sandbox. Do not commit it. - For short tasks, run
tallygraph collectwhen the task ends. - For long sessions, start
tallygraph collect --watch --interval 60in a second terminal or as a background process. - If the sandbox can restart with a clean disk, keep the agent history and
TALLYGRAPH_STATE_DIRon a persistent volume. Otherwise usage not sent before the restart is lost with the history.
npm i -g tallygraph tallygraph collect --dry-run # sends nothing tallygraph collect --watch --interval 60
Running collect more than once is safe. The API drops events it already has, by external_id.
Kubernetes
Sidecar in the agent pod
Add the collector as a second container in the pod that runs the agent. Both mount the same volume at the history path. The collector mounts it read-only and keeps its spool on its own volume. Build the image from the Dockerfile above and push it to your registry. Create the Secret first:
kubectl create secret generic tallygraph --from-literal=write-token="$TALLYGRAPH_WRITE_TOKEN"
apiVersion: v1
kind: Pod
metadata:
name: agent
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
volumes:
- name: claude-history
emptyDir: {}
- name: tallygraph-state
emptyDir: {}
containers:
- name: agent
image: your-agent-image
env:
- name: HOME
value: /home/node
volumeMounts:
- name: claude-history
mountPath: /home/node/.claude/projects
- name: tallygraph
image: registry.example.com/tallygraph-collector
args: ["collect", "--watch", "--interval", "300"]
env:
- name: HOME
value: /home/node
- name: TALLYGRAPH_STATE_DIR
value: /var/lib/tallygraph
- name: TALLYGRAPH_WRITE_TOKEN
valueFrom:
secretKeyRef:
name: tallygraph
key: write-token
volumeMounts:
- name: claude-history
mountPath: /home/node/.claude/projects
readOnly: true
- name: tallygraph-state
mountPath: /var/lib/tallygraph- Change
/home/nodeto the agent HOME. The collector looks in the paths under its ownHOME, so both must match the mount. - For Codex, mount
~/.codex/sessionsthe same way. emptyDirlasts as long as the pod. Use a PersistentVolumeClaim for both volumes if the pod is replaced before the collector catches up.- If the agent runs as a Job, do not add a long-running sidecar, because the Job will not finish. Run
tallygraph collectat the end of the agent command instead.
CronJob
Only if the history is on a PersistentVolumeClaim the job can mount. A ReadWriteOnce volume can only be mounted by pods on the same node as the agent.
apiVersion: batch/v1
kind: CronJob
metadata:
name: tallygraph-collect
spec:
schedule: "*/15 * * * *"
concurrencyPolicy: Forbid
jobTemplate:
spec:
template:
spec:
restartPolicy: Never
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
volumes:
- name: claude-history
persistentVolumeClaim:
claimName: agent-claude-history
readOnly: true
- name: tallygraph-state
persistentVolumeClaim:
claimName: tallygraph-state
containers:
- name: tallygraph
image: registry.example.com/tallygraph-collector
args: ["collect"]
env:
- name: HOME
value: /home/node
- name: TALLYGRAPH_STATE_DIR
value: /var/lib/tallygraph
- name: TALLYGRAPH_WRITE_TOKEN
valueFrom:
secretKeyRef:
name: tallygraph
key: write-token
volumeMounts:
- name: claude-history
mountPath: /home/node/.claude/projects
readOnly: true
- name: tallygraph-state
mountPath: /var/lib/tallygraphTroubleshooting
| You see | What it means | Fix |
|---|---|---|
no local sources found | Exit code 2. No history at the default paths under HOME: the volume is not mounted, it is mounted at another path, or HOME points somewhere else. | Check HOME and the mount path, then run collect --dry-run until it prints discovered. |
missing --token (or TALLYGRAPH_WRITE_TOKEN env) | The token did not reach the container. | Check that the variable is set where you run docker, or the Secret name and key in secretKeyRef. |
cannot open spool at … | Exit code 3. The state directory is not writable by the container user. | Mount a writable volume there. On Kubernetes, set fsGroup. |
watch: tick N produced no events | In --watch mode the process keeps running and retries on the next tick. | Same fixes as no local sources found. |