TALLYGRAPHUNIFIED SPEND CONSOLE

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-slim image 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 in TALLYGRAPH_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.

terminal
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.

Dockerfile
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"]
terminal
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 with npx --yes tallygraph.
  • Set TALLYGRAPH_WRITE_TOKEN as a secret or environment variable of the sandbox. Do not commit it.
  • For short tasks, run tallygraph collect when the task ends.
  • For long sessions, start tallygraph collect --watch --interval 60 in a second terminal or as a background process.
  • If the sandbox can restart with a clean disk, keep the agent history and TALLYGRAPH_STATE_DIR on a persistent volume. Otherwise usage not sent before the restart is lost with the history.
terminal
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:

terminal
kubectl create secret generic tallygraph --from-literal=write-token="$TALLYGRAPH_WRITE_TOKEN"
pod.yaml
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/node to the agent HOME. The collector looks in the paths under its own HOME, so both must match the mount.
  • For Codex, mount ~/.codex/sessions the same way.
  • emptyDir lasts 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 collect at 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.

cronjob.yaml
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/tallygraph

Troubleshooting

You seeWhat it meansFix
no local sources foundExit 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 eventsIn --watch mode the process keeps running and retries on the next tick.Same fixes as no local sources found.
Tested with Docker and the published CLI. It has not been tested on Daytona or on a real Kubernetes cluster yet: the manifests above were only checked against the Kubernetes schema.