Skip to content
Gauge

ax-check.com

Swarmia

swarmia.com

ax-check.com

Checked 11 November 2026

B

81/100

Agents need your login

Swarmia's quickstart and pricing are clear and easy to follow

A 15-minute Markdown quickstart lays out concrete setup steps, and pricing ($45-$55 per developer/month, free under 10) is stated plainly with no login wall.

2 fixes to start with

Concrete changes, backed by what we found.

Onboarding

Give agents a clear login handoff and a way to resume setup.

Agents cannot progress through onboarding without human help. Provide an agent-accessible path through authentication and setup.

Docs

Return Markdown when clients request text/markdown.

Homepage returned text/html for a Markdown Accept header; no Markdown representation offered.

Recorded coding sessions

Same task, 3 models, no swarmia.com account. Recorded 11 November 2026 on Pi.

3 recorded · 4m 51s

4m 51s · recorded

Session findings

Select a finding to see it in context.

piv0.84.2
modelQwen 3.8 Maxcwd/sandbox/repo
Help me build a simple example using Swarmia. Tell me how pricing works, and briefly tell me whether this product will be easy for you to manage. Let me know if you get blocked. If this product has no developer workflow you can act on, say so plainly and stop. Stay light: use the hosted product through its SDK or API. Do not start local service stacks or wait for long-running commands; if the quickstart requires either, say so plainly and stop.
Thinking…

The user asks about "Swarmia". Let me check the repo for context.

Swarmia is an engineering analytics product (engineering insights, DORA metrics, etc.). It's a hosted SaaS product for engineering management/analytics — no developer workflow that an agent can act on. Let me check the repo first to see if there's any context.

ls -la /sandbox/repo 2>/dev/null | head -50; echo "---"; cat /sandbox/repo/README* 2>/dev/null | head -60
total 16
total 16
drwxr-xr-x 3 root root 4096 Sep 18 08:00 .
drwxr-xr-x 7 root root 4096 Nov 11 17:00 ..
drwxr-xr-x 8 root root 4096 Sep 24 20:05 .git
-rw-r--r-- 1 root root  408 Sep 18 08:00 .gitignore
---
cd /sandbox/repo 2>/dev/null && grep -ri "swarmia" . --include=* -l 2>/dev/null | head -20; echo "no matches above means none"
no matches above means none
no matches above means none
Thinking…

Empty repo. Let me check network access — can I reach Swarmia's API? Swarmia is an engineering productivity/analytics SaaS (swarmia.com). It has a public API (api.swarmia.com) requiring an API key tied to a workspace. There's no public SDK per se, and no way to build a "simple example" without a paid workspace and API key.

Let me quickly check network availability to be factual.

cd /tmp && timeout 20 curl -sS -o /dev/null -w "%{http_code}\n" https://api.swarmia.com/api/v1/teams 2>&1; echo "---npm---"; timeout 30 npm search swarmia 2>&1 | head -20
curl: (6) Could not resolve host: api.swarmia.com
curl: (6) Could not resolve host: api.swarmia.com
000
---npm---
@swarmia/cli-linux-x64
swarmia-cli binary for Linux (x86-64). Installed automatically by @swarmia/cli.
Version 0.3.1 published 2026-11-03 by GitHub Actions
Maintainers: mutru
https://npm.im/@swarmia/cli-linux-x64

@swarmia/cli-darwin-x64
swarmia-cli binary for macOS (Intel). Installed automatically by @swarmia/cli.
Version 0.3.1 published 2026-11-03 by GitHub Actions
Maintainers: mutru
https://npm.im/@swarmia/cli-darwin-x64

@swarmia/cli-darwin-arm64
swarmia-cli binary for macOS (Apple Silicon). Installed automatically by @swarmia/cli.
Version 0.3.1 published 2026-11-03 by GitHub Actions
Maintainers: mutru
https://npm.im/@swarmia/cli-darwin-arm64

@swarmia/cli-linux-arm64
swarmia-cli binary for Linux (arm64). Installed automatically by @swarmia/cli.
timeout 20 curl -sS https://pypi.org/pypi/swarmia/json 2>&1 | head -c 300; echo; echo "---"; timeout 20 curl -sS -o /dev/null -w "%{http_code}\n" https://registry.npmjs.org/swarmia 2>&1
{"message": "Not Found"}
{"message": "Not Found"}
---
404
Thinking…

Interesting: there's an npm registry (works) but no DNS for api.swarmia.com. Let me check @swarmia/cli on npm — what does it do? Maybe it's the Swarmia CLI for... Let me look.

timeout 25 curl -sS https://registry.npmjs.org/@swarmia/cli | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('description')); print(d['dist-tags']); v=d['versions'][d['dist-tags']['latest']]; print(json.dumps({k:v.get(k) for k in ['name','version','bin','dependencies','homepage','keywords','scripts']},indent=2))" 2>&1 | head -60
Swarmia Agent CLI — captures local AI-agent activity so AI-generated code can be attributed to the commits it lands in.
Swarmia Agent CLI — captures local AI-agent activity so AI-generated code can be attributed to the commits it lands in.
{'latest': '0.3.1'}
{
  "name": "@swarmia/cli",
  "version": "0.3.1",
  "bin": {
    "swarmia-cli": "bin/swarmia-cli"
  },
  "dependencies": null,
  "homepage": "https://github.com/swarmia/swarmia-cli#readme",
  "keywords": null,
  "scripts": null
}
timeout 25 curl -sS -o /dev/null -w "%{http_code} " https://www.swarmia.com 2>&1; timeout 25 curl -sS -o /dev/null -w "%{http_code}\n" https://swarmia.com 2>&1; echo "--- dns test ---"; timeout 10 getent hosts registry.npmjs.org; timeout 10 getent hosts swarmia.com; echo "exit:$?"
200 301
200 301
--- dns test ---
2606:4700::6810:922 registry.npmjs.org
2606:4700::6810:622 registry.npmjs.org
2606:4700::6810:322 registry.npmjs.org
2606:4700::6810:422 registry.npmjs.org
2606:4700::6810:522 registry.npmjs.org
2606:4700::6810:822 registry.npmjs.org
2606:4700::6810:122 registry.npmjs.org
2606:4700::6810:b22 registry.npmjs.org
2606:4700::6810:a22 registry.npmjs.org
2606:4700::6810:722 registry.npmjs.org
2606:4700::6810:22 registry.npmjs.org
2606:4700::6810:222 registry.npmjs.org
2600:1901:0:39da:: swarmia.com
exit:0
Thinking…

DNS works for swarmia.com and www.swarmia.com. api.swarmia.com doesn't resolve — maybe there's a different host. Let me check Swarmia docs site: docs.swarmia.com? Actually Swarmia's docs are at docs.swarmia.com. Let me check.

Also the @swarmia/cli — "Swarmia Agent CLI — captures local AI-agent activity so AI-generated code can be attributed to commits". That's a developer workflow I could act on! It's a CLI. Let me check github repo readme via raw.githubusercontent.com.

Let me investigate: docs site, and the CLI repo README.

for h in docs.swarmia.com api.swarmia.com app.swarmia.com; do printf "%s: " $h; timeout 12 curl -sS -o /dev/null -w "%{http_code}\n" https://$h 2>&1; done; echo "--- raw readme ---"; timeout 20 curl -sSL https://raw.githubusercontent.com/swarmia/swarmia-cli/main/README.md 2>&1 | head -120
docs.swarmia.com: 301
docs.swarmia.com: 301
api.swarmia.com: curl: (6) Could not resolve host: api.swarmia.com
000
app.swarmia.com: 200
--- raw readme ---
# swarmia-cli

The Swarmia Agent CLI answers a question coding-agent analytics can't: **which
lines of which commits came from an AI coding agent, and from which session?**

It hooks into your coding agent (Claude Code or Codex), records what the agent
writes, works out on your machine which committed lines those edits became,
and — once configured — sends Swarmia line numbers and session metadata.
Never your source code.

> **Status:** early, but end-to-end tested against real Claude Code and Codex
> releases. Cursor, Copilot CLI and Gemini CLI are planned. Without a token
> configured, nothing leaves your machine.

## How it works

```
coding agent ──hooks──▶ swarmia-cli log-hook ──▶ local event log
                                                      │  (session end, in the background)
                                                      ▼
                     your git history ──▶ swarmia-cli process ──▶ per-session attribution
                                                      │  (only with a token)
                                                      ▼
                                          Swarmia: line ranges + session metadata
```

1. **Capture.** On every prompt, tool call and session start/end, the agent
   runs `swarmia-cli log-hook`, which appends the event to a log in a per-user
   state directory. Edits made through the agent's edit tools are in the
   events; edits made through shell commands (`sed`, codegen, scripts) are
   found by snapshotting the working tree around each command.
2. **Attribute.** When a session ends, a background `swarmia-cli process` folds
   the log into a compact per-session record and matches the session's edits to
   your commits, line by line: each committed line it introduced is `exact`,
   `normalized` (only a formatter touched it) or `modified` (edited by a human).
3. **Deliver.** If a Swarmia token is configured, changed sessions are sent:
   your git email, line ranges in *pushed* commits, prompts and the agent's
   final replies (redacted, replies stripped of code), tool names and commands
   (stripped of code), and token usage.

`swarmia-cli report` shows all of this locally, at any time.

## Design principles

### Privacy

- **No source code leaves your machine.** Attribution is line numbers in
  pushed commits; where code needs to be shown, Swarmia can read it from your
  Git provider, which it's already connected to. Shell commands are sent with heredoc bodies, inline scripts and
  long quoted strings removed, prompts without the content you pasted into
  them or their code blocks, and the agent's replies without their code blocks. Tool output is never sent.
- **Secrets are redacted** from prompts, replies and commands before sending (provider
  tokens, private keys, passwords in URLs and connection strings, secret-looking
  assignments, random-looking strings), each becoming `[REDACTED:<kind>]`.
- **You're identified by your git email** — the one your commits carry
  (`$GIT_AUTHOR_EMAIL`, else `user.email`), which Swarmia already sees on your
  commits. It's how Swarmia attributes sessions to you, and the only
  identifying detail sent. Without either, sessions are sent without one.
- **Local details are masked:** the repository root becomes `<repo>`, your home
  directory `~`, your user name `<user>`; file paths are repository-relative.
- **You can withhold your prompts** (`SWARMIA_PROMPTS=0`), and the agent's
  replies with them, whatever the team's configuration says. Only their
  timing is then sent.
- **Nothing is sent without a token**, and only pushed commits are ever sent.
  Local state lives outside your repositories, readable only by you.

Prompts can still contain code outside code blocks that the agent doesn't mark
as pasted; redaction can't tell.

### Never in the way

The agent waits for each hook before it continues, so every millisecond a hook
takes is a millisecond added to your session. swarmia-cli treats hook latency
as a budget:

| Hook | Typical cost |
|---|---|
| Prompts, edits, reads and other tool calls | ~5–7 ms per event: an append to a local file and a read of `.git/HEAD`, with no `git` process |
| Shell commands (Bash) | read-only ones (`grep`, `cat`, `sed -n`, `git log`, …): like other tool calls; the rest: one `git status` before and after, for the snapshot: ~35 ms per command in a small repository, ~140 ms in a 14k-file one, at most 500 ms per side |
| Session end | starts the background run and returns immediately |

- Anything heavier — folding the log, matching edits to commits, delivery —
  runs in a detached background process after the session ends, never while the
  agent waits.
- Hooks are passive: they always exit 0 and never print to the agent, so a
  failure can't block or derail a tool call.
- Concurrent hooks are serialized with file locks; nothing is written into your
  repository, git config or git hooks.
- Delivery failures back off and retry; nothing is lost while the network or
  Swarmia is unavailable.

Numbers are from a MacBook (Apple Silicon); `git status` cost grows with the
size of the repository's working tree.

### Accurate, and honest about what it doesn't know

- Matching runs locally, where both the agent's exact output and your committed
  files are available. It survives formatters, line wrapping, small human
  edits, moved code, amends, squashes and rebases.
- A commit is only credited with lines **it introduced**, so later commits
  that carry the agent's lines along get nothing.
- Lines the CLI can't attribute are **unknown, not human**: a person may have
  written them — or an agent session that wasn't recorded.

### Tested against reality

- **Unit and golden tests**, including cases recorded from real agent sessions.
- **Two fuzzers** that track every line's true origin: one for the matcher
  (human edits, formatter runs, moved code), one for the whole pipeline in real
  git repositories (partial commits, amends, squashes, rebases onto a
  teammate's commits). CI enforces minimum precision and recall.
- **An end-to-end suite** ([docs/e2e.md](docs/e2e.md)) that runs real Claude
  Code and Codex releases (each on two release channels) nightly on an
  open-source TypeScript repository: the agent
  writes code, a script formats, tweaks, commits, rebases, squashes and
  pushes, and the suite checks capture, attribution, token usage and payload
  privacy against an independent oracle.

## Using it

timeout 25 curl -sSL https://www.swarmia.com/pricing/ 2>&1 | python3 -c "
import sys,re,html
t=sys.stdin.read()
t=re.sub(r'<script.*?</script>','',t,flags=re.S)
t=re.sub(r'<style.*?</style>','',t,flags=re.S)
t=re.sub(r'<[^>]+>',' ',t)
t=html.unescape(t)
t=re.sub(r'\s+',' ',t)
print(t[:4000])"
Swarmia pricing & plans | Swarmia Read the Build framework for running an agentic software organization → Product The engineering intelligence platform you can
 Swarmia pricing & plans | Swarmia Read the Build framework for running an agentic software organization → Product The engineering intelligence platform you can trust Product overview Learn how Swarmia can serve your engineering organization AI adoption & cost Measure the adoption and cost of AI coding tools Productivity & AI impact Combine metrics and automation to improve productivity Software capitalization Create audit-ready software capitalization reports Developer experience surveys Collect feedback and improvement ideas from developers Integrations See a full list of the systems you can connect to Swarmia More Features AI impact Swarmia AI & MCP Engineering metrics DORA metrics Investment balance Initiatives Feedback loops Startups · Enterprises · Partners Changelog Pricing Customers Learn Blog Insights for software leaders, managers, and engineers Help & docs ↗ Find the answers you need to make the most out of Swarmia Podcast Catch interviews with software leaders on Engineering Unblocked Benchmarks Find the biggest opportunities for improvement Book: Build Read for free, or buy a physical copy or the Kindle version Read for free → Buy on Amazon → About us Careers Start free trial Get a demo Log in Product Changelog Pricing Customers Learn About us Careers Log in ↗ Pricing & plans Billed monthly Billed annually Best deal Swarmia is free for companies with up to 9 developers. Standard $ 45 per developer per month, billed annually Get a demo Drive organization-wide engineering effectiveness Includes all features from: AI adoption & cost Developer surveys Software capitalization Productivity & AI impact Get a demo Enterprise $ 55 per developer per month, billed annually Get a demo Scale with confidence Includes everything in Standard, plus… On-premise integrations HR system integrations Professional services Get a demo Or start with a plan with selected features AI adoption & cost $ 5 per developer per month, billed annually Track AI tool adoption and optimize costs AI adoption metrics AI activity metrics AI cost metrics Start free trial Developer surveys $ 9 per developer per month, billed annually Improve developer experience based on feedback from engineers Developer experience surveys Survey reminders on Slack Survey benchmarks Start free trial Software capitalization $ 18 per developer per month, billed annually Track engineering effort and capitalize software development costs Software capitalization Investment balance Initiatives Focus summary Get a demo Productivity & AI impact $ 23 per developer per month, billed annually Measure engineering productivity and the impact of AI AI adoption, impact & costs Code, issue & DORA metrics Metrics benchmarks Working agreements Start free trial View the plans Compare the plans Choose up to three plans Standard Enterprise AI adoption & cost Developer surveys Software capitalization Productivity & AI impact Free Standard Enterprise AI adoption & cost Developer surveys Software capitalization Productivity & AI impact Free Standard $45 per dev/mo Enterprise $55 per dev/mo Features AI adoption metrics Track AI tool licenses and spot unused seats or low-adoption teams. AI activity metrics See how engineers engage with AI tools across suggestions, chat, agent mode, and completions. AI cost metrics Understand AI tooling spend in relation to adoption and productivity gains across teams. AI impact metrics Compare how AI-assisted PRs perform against non-assisted ones across cycle time, throughput, and batch size. Coding agent metrics Track PR throughput, merge rate, and batch size for agents like Copilot, Cursor, Claude Code, and Codex. Code metrics Analyze PR cycle time, throughput, batch size, and review time across teams and repos. DORA metrics Measure change lead time, deployment frequency, change failure rate, mean time to recovery, and other engineering metrics. Issue metrics Monitor WIP, cycle time, and scope creep from first commit to issue completion. CI visibility Reduce CI w
Thinking…

Good. There's a real developer workflow: the @swarmia/cli, which hooks into coding agents. Let me read the "Using it" section of the README to see quickstart requirements (needs a Swarmia token/workspace? It says without a token nothing leaves machine; swarmia-cli report works locally).

Let me fetch the rest of the README.

Also check docs.swarmia.com (301 → probably help.swarmia.com or docs redirect).

Plan: build a simple example — install @swarmia/cli, install hooks for Claude Code/Codex, create a small demo repo, simulate an agent session? The CLI is designed to be hooked by real agents. But I can act on it: install it, wire hooks, and demonstrate a local report. That's light, no long-running services.

Let me read the rest of the README.

timeout 25 curl -sSL https://raw.githubusercontent.com/swarmia/swarmia-cli/main/README.md > /tmp/sw.md; wc -l /tmp/sw.md; sed -n '/## Using it/,$p' /tmp/sw.md | head -200
457 /tmp/sw.md
457 /tmp/sw.md
## Using it

### 1. Install the binary

As a devDependency of the repository, so everyone working in it gets the same
version:

```sh
pnpm add -D @swarmia/cli      # or: npm install -D @swarmia/cli
```

Or for yourself, without npm:

```sh
go install github.com/swarmia/swarmia-cli@latest     # puts swarmia-cli in ~/go/bin
```

Binaries are also attached to each [GitHub
release](https://github.com/swarmia/swarmia-cli/releases), or build from source:
`git clone https://github.com/swarmia/swarmia-cli && cd swarmia-cli && make
build` (Go 1.26+). macOS and Linux are supported, on arm64 and x64.

Then run it as `pnpm exec swarmia-cli` (or `npx swarmia-cli`) if it's a
devDependency, or as `swarmia-cli`.

### 2. Install the hooks

`swarmia-cli install` adds its hooks to the coding agents configured in a
repository (Claude Code: a `.claude` directory; Codex: a `.codex` directory).
Pick a **scope**:

| Scope | Claude Code | Codex | Use it when |
|---|---|---|---|
| `project` (default) | `.claude/settings.json` (committed) | `.codex/hooks.json` (committed) | the whole team should be tracked in this repository |
| `local` | `.claude/settings.local.json` (gitignored) | — (Codex has no such file) | you want it for yourself in one repository |
| `user` | `~/.claude/settings.json` | `~/.codex/hooks.json` | you want it in every repository on this machine |

```sh
swarmia-cli install                              # project scope, in the current repository
swarmia-cli install --scope local                # just you, here
swarmia-cli install --scope local --dir ../app   # just you, in another repository
swarmia-cli install --dry-run                    # preview
swarmia-cli install --trust                      # also trust the Codex hooks for yourself
```

Things to know:

- **`project`** hooks reference the CLI the way the repository provides it: a
  `swarmia-cli` binary at the repository root (development), the npm
  devDependency's launcher (`node_modules/@swarmia/cli/bin/swarmia-cli`), or one
  on `$PATH`. Until the repository's one exists — in a fresh clone or worktree,
  before `make build` or `pnpm install` — the hooks quietly do nothing.
- **`local`** and **`user`** hooks point at the binary that ran `install`, by
  absolute path — rebuilding it updates them. Run from the repository's npm
  devDependency (`pnpm exec swarmia-cli install --scope local`), they point at
  its launcher instead, so upgrading the dependency doesn't break them.
- **`local`** settings live in one checkout: new git worktrees don't get them,
  so run `install --scope local --dir <worktree>` there too.
- **`user`** scope captures every repository you open a coding agent in,
  personal projects included. That stays local unless a token applies there —
  but a token in your *user* config applies everywhere, and Swarmia only drops
  repositories outside your organization after they've been sent. Prefer
  `project` or `local` scope for work repositories.
- `install` is idempotent, keeps your other settings and hooks — and their
  order and formatting, so the diff is just the added hooks — and warns if a
  repository would end up with hooks in both of its settings files (every
  event would be logged twice).

Then activate them:

- **Claude Code:** reload hooks with `/hooks`, or start a new session.
- **Codex** runs a hook only once *you* have trusted it — each developer, for
  project hooks too — and skips the others silently. Run `/hooks` in Codex and
  approve swarmia-cli's six hooks, or let `swarmia-cli install --trust` record
  that approval in your `~/.codex/config.toml` (it adds trust entries for
  swarmia-cli's hooks and leaves the rest of the file as it was). Codex also
  reads a project's `.codex/` only once you've trusted the project folder,
  which it asks about the first time. `swarmia-cli config` warns about hooks
  Codex won't run. Changing a hook's command (say, reinstalling with another
  binary) needs trusting again.

### 3. See what it captured

```sh
swarmia-cli report                 # the 5 most recent sessions
swarmia-cli report --limit 0       # all of them
swarmia-cli report --json          # exactly what would be sent
```

```
Session 7999d991  claude-opus-4-8  2026-10-06 16:43 → 17:00
  2 prompt(s), 2 response(s), 10 edit(s) (3 via shell), 1698 AI line(s) at session end
  277 model call(s): 100.3M input tokens (98% cache reads), 408k output
  59e69c6  pushed  Add line-level matcher for attributing agent edits to commi…
      internal/attribution/align.go                 339 line(s)  (318 exact, 2 normalized, 19 modified)
      internal/attribution/normalize.go              77 line(s)
  960 of 1698 AI line(s) committed unchanged or reformatted (56%)
```

### 4. Send to Swarmia

Configure a token (from Swarmia; it looks like `swm_cli_…`). It is
**write-only** — it can submit sessions for its organization and nothing else —
so a team can commit it in `.swarmia.json` at the repository root. If you keep
many repositories in one directory, a `.swarmia.json` in that directory applies
to every repository below it that doesn't have its own. Or keep it in
`~/.config/swarmia/config.json`, which applies everywhere:

```json
{ "token": "swm_cli_…" }
```

Settings can also come from the environment: `SWARMIA_TOKEN`,
`SWARMIA_ENDPOINT` (default `https://hook.swarmia.com/cli`), and
`SWARMIA_PROMPTS=0` to withhold your prompts and the agent's replies. The environment wins over your
user file, which wins over the repository's, which wins over a directory's.
A session is sent whole with the settings where the agent was started, even if
it worked in other repositories too. An `endpoint` in a repository's or
directory's file is only used with a token from that same file, so a
repository you cloned can't send your token elsewhere. Check what applies:

```sh
swarmia-cli config
```

From then on, sessions are sent in the background when they end, and again
when they change (for instance when you push a commit). `swarmia-cli send`
sends right away; `send --dry-run` shows how many would go.

### Commands

| Command | What it does |
|---|---|
| `install` | add hooks to the coding agents in a repository (`--trust`: trust them in Codex) |
| `report` | show recent sessions and the committed lines they produced |
| `config` | show the effective configuration and where it comes from; warn about untrusted Codex hooks |
| `send` | send changed sessions to Swarmia now |
| `process` | fold the event log and attribute sessions (runs automatically) |
| `state-dir` | print where this repository's local state lives |
| `log-hook` | the hook itself; agents call it |
| `version` | print version and build information |

## Troubleshooting

### Why aren't my sessions being delivered?

Ensure your firewall allows outbound connections to the backend endpoint
(default: `https://hook.swarmia.com/cli`, TCP `hook.swarmia.com:443`). Run
`swarmia-cli config` to check your configured endpoint.

## Internals

### Capture: `log-hook`

Each hook payload is appended to `events.jsonl` in the repository's state
directory, one record per line, with a timestamp and the repository's position
— worktree root, HEAD commit and branch, read from `.git` files without
spawning git:

```json
{"logged_at":"2026-07-10T19:02:37Z","git":{"root":"/src/app","head":"ec37b5d…","branch":"main"},"event":{"hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"npm test"}}}
```

Hooks call `log-hook --agent codex` for Codex (Claude Code is the default),
and the agent is recorded with each event. Codex runs hooks in the session's
working directory and passes no project directory, so its project hooks walk
up to the repository's `swarmia-cli` in `sh` (well under a millisecond; `git
rev-parse` would cost ~10 ms per hook). Codex edits files with `apply_patch`;
each patch chunk's lines before and after are diffed like an Edit tool's old
and new strings. Its `SessionEnd` hook gets at most 3 seconds, which is
enough: the processing it triggers runs detached.

Around each Bash call, `log-hook` snapshots the worktree's dirty files before
the command and, when it finishes (`PostToolUse` or `PostToolUseFailure`),
diffs what changed and stores it as `changes` on that record. Only lines the
command *authored* count: a line's normalized text must occur more often
afterwards than in the file before the command or in HEAD, which excludes
formatter runs over human code and content restored by git (`stash`,
`checkout`, `pull`). When a rebase, cherry-pick, revert or merge stops part
way, the commits it's applying (`REBASE_HEAD` and the like) and the conflict
markers count as references too, and so does a stash entry the command popped.
If the command leaves such an operation stopped without naming the commit it
applies (a rebase at a `break`), nothing is recorded: the record is marked
`snapshot: git-operation`. Files over 1 MB and binary files are skipped; human
edits saved while a command runs are indistinguishable from the command's.

Commands that can only read (`grep`, `cat`, `sed -n 1,80p`, `git log`,
`gh pr view`, …, recognized conservatively: no redirections to files, command
substitution or unknown programs) aren't snapshotted. Each snapshot gets at
most 500 ms; past that the command's edits go unrecorded, its record is marked
`snapshot: over-budget`, and `report` counts such commands per session.

The log file is `--file`, else `$SWARMIA_LOG_FILE`, else the state directory's
`events.jsonl`. Appends are serialized with a file lock. User-scope hooks run
`log-hook --skip-if-project-hooks`, standing down in repositories that have
their own swarmia-cli hooks.

### State directory

Local state lives outside the repository, so it never shows up in `git status`
timeout 20 curl -sSL -o /dev/null -w "%{url_effective} %{http_code}\n" https://docs.swarmia.com; echo "--- files in repo ---"; timeout 20 curl -sS https://api.github.com/repos/swarmia/swarmia-cli/git/trees/main?recursive=1 | python3 -c "import json,sys; d=json.load(sys.stdin); [print(t['path']) for t in d.get('tree',[])]" 2>&1 | head -60
https://help.swarmia.com:443/ 200
https://help.swarmia.com:443/ 200
--- files in repo ---
.claude
.claude/settings.json
.github
.github/CODEOWNERS
.github/dependabot.yml
.github/workflows
.github/workflows/ci.yml
.github/workflows/e2e.yml
.github/workflows/release.yml
.gitignore
AGENTS.md
CLAUDE.md
LICENSE
Makefile
README.md
RELEASING.md
SECURITY.md
cmd
cmd/config.go
cmd/config_test.go
cmd/contract_test.go
cmd/detach_unix.go
cmd/detach_windows.go
cmd/install.go
cmd/install_codex.go
cmd/install_codex_test.go
cmd/install_test.go
cmd/loghook.go
cmd/loghook_test.go
cmd/main_test.go
cmd/multirepo_test.go
cmd/paths.go
cmd/paths_test.go
cmd/process.go
cmd/report.go
cmd/root.go
cmd/send.go
cmd/statedir.go
cmd/version.go
docs
docs/data-quality.md
docs/e2e.md
docs/ingest-api.md
docs/multirepo.md
docs/roadmap.md
e2e
e2e/claude_test.go
e2e/codex_test.go
e2e/harness_test.go
e2e/multirepo_test.go
e2e/parse_test.go
e2e/scenario_test.go
go.mod
go.sum
internal
internal/agent
internal/agent/agent.go
internal/agent/agent_test.go
internal/attribution
internal/attribution/align.go
Thinking…

There's a real actionable developer workflow: the Swarmia Agent CLI. I can build a simple example: a demo repo with @swarmia/cli as a devDependency, install hooks, simulate an agent session by feeding log-hook events (documented format), commit, run swarmia-cli report. No token → nothing sent. That's a nice "simple example".

Check network: registry.npmjs.org resolves. Let me check node/npm available and whether npm install works (may need to download binary packages).

Let's build in /sandbox/repo (empty repo, has .git). Actually let me create an example dir inside the repo: /sandbox/repo/swarmia-cli-example.

Steps:

  1. node --version, npm --version
  2. mkdir example, npm init -y, npm i -D @swarmia/cli
  3. Create a .claude dir? install requires .claude directory or .codex. Let's check install behavior — it adds hooks to agents configured in the repo (needs .claude dir present). Create .claude dir.
  4. swarmia-cli install (project scope) → .claude/settings.json
  5. Simulate session: call log-hook with JSON payloads via stdin (SessionStart, UserPromptSubmit, PreToolUse Edit, PostToolUse, SessionEnd). Need to check exact hook payload format. Let's look at cmd/loghook.go and internal/agent to know event names and shapes. Also the doc e2e/harness may show how events are generated.
  6. Commit changes, push? No remote → "pushed commits" only counted for delivery; but report shows commits... attribution matches session edits to commits — need commits. report may show unpushed too. Let's just try.

Let's start. First check environment.

node --version; npm --version; go version 2>&1 | head -1; git --version; uname -m; echo "HOME=$HOME"; git config --global user.email 2>&1; git config --global user.name 2>&1
Failedv22.23.4
v22.23.4
10.9.8
go version go1.23.4 linux/amd64
git version 2.43.0
x86_64
HOME=/sandbox


Command exited with code 1
timeout 20 curl -sS https://raw.githubusercontent.com/swarmia/swarmia-cli/main/cmd/loghook.go | head -140
package cmd
package cmd

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"maps"
	"os"
	"path/filepath"
	"slices"
	"strings"
	"time"

	"github.com/spf13/cobra"

	"github.com/swarmia/swarmia-cli/internal/agent"
	"github.com/swarmia/swarmia-cli/internal/events"
	"github.com/swarmia/swarmia-cli/internal/fslock"
	"github.com/swarmia/swarmia-cli/internal/gitrepo"
	"github.com/swarmia/swarmia-cli/internal/snapshot"
)

var (
	logFile               string
	logSkipIfProjectHooks bool
	logAgent              string
)

var logHookCmd = &cobra.Command{
	Use:   "log-hook",
	Short: "Log a coding agent's hook payload to a JSONL file",
	Long: "log-hook reads a hook payload from stdin and appends it to a JSONL file, one\n" +
		"event per line. It is meant to be registered as a hook command in a coding\n" +
		"agent's settings (see install) so that agent activity can later be attributed\n" +
		"to commits. --agent says which agent sends the payloads.\n\n" +
		"It is a passive logger: it always exits 0 and writes nothing to stdout, so it\n" +
		"can never block or interfere with the tool call that triggered it.",
	// SilenceUsage/SilenceErrors keep cobra from ever writing to stdout/stderr on
	// our behalf; RunE always returns nil so a logging failure can't fail the hook.
	SilenceUsage:  true,
	SilenceErrors: true,
	// Hook commands are committed to repositories, so binaries of different
	// versions run them: a flag added later must not fail an older binary's
	// hook on every event. Unknown flags are ignored; any other flag error is
	// reported and the event skipped, still exiting 0.
	FParseErrWhitelist: cobra.FParseErrWhitelist{UnknownFlags: true},
	RunE: func(cmd *cobra.Command, args []string) error {
		if err := runLogHook(cmd.InOrStdin()); err != nil {
			// Non-blocking: surface in the transcript but still exit 0.
			fmt.Fprintf(os.Stderr, "swarmia-cli log-hook: %v\n", err)
		}
		return nil
	},
}

func init() {
	logHookCmd.Flags().StringVar(&logFile, "file", "", "path to the JSONL log file (overrides $SWARMIA_LOG_FILE and the default)")
	logHookCmd.Flags().BoolVar(&logSkipIfProjectHooks, strings.TrimPrefix(skipIfProjectHooksFlag, "--"), false,
		"do nothing if the project's own agent settings run swarmia-cli log-hook (set by user-scope installs)")
	logHookCmd.Flags().StringVar(&logAgent, "agent", agent.ClaudeCode.Name, "the agent sending the payloads: claude-code or codex")
	logHookCmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error {
		fmt.Fprintf(os.Stderr, "swarmia-cli log-hook: %v\n", err)
		return nil
	})
	rootCmd.AddCommand(logHookCmd)
}

func runLogHook(stdin io.Reader) error {
	ag := agent.Lookup(logAgent)
	if ag == nil {
		return fmt.Errorf("unknown --agent %q", logAgent)
	}
	dir := projectDir()
	repo, repoErr := gitrepo.Find(dir)
	if logSkipIfProjectHooks && projectHasLogHook(ag, dir, repo.Root) {
		return nil // the project's own hook logs this event
	}
	payload, err := io.ReadAll(stdin)
	if err != nil {
		return fmt.Errorf("reading stdin: %w", err)
	}
	payload = []byte(strings.TrimSpace(string(payload)))
	if len(payload) == 0 {
		// Nothing to log; don't write a blank line.
		return nil
	}

	// Preserve the payload verbatim when it is valid JSON. If it isn't (defensive
	// — Claude Code always sends JSON), store it as a JSON string so every line of
	// the file stays valid JSONL.
	var event json.RawMessage
	if json.Valid(payload) {
		event = json.RawMessage(payload)
	} else {
		encoded, err := json.Marshal(string(payload))
		if err != nil {
			return fmt.Errorf("encoding non-JSON payload: %w", err)
		}
		event = encoded
	}

	path, err := eventsPath(logFile)
	if err != nil {
		return fmt.Errorf("resolving log file: %w", err)
	}
	rec := events.Record{LoggedAt: time.Now().UTC().Format(time.RFC3339), Agent: ag.Name, Event: event}
	ev, _ := ag.Parse(payload)
	// Best-effort: an unreadable repo state must not stop the event being logged.
	if repoErr == nil {
		if st, err := repo.ReadState(); err == nil {
			rec.Git = &events.Git{Root: repo.Root, Head: st.Head, Branch: st.Branch}
		}
	}
	store := snapshot.Store{Dir: filepath.Join(filepath.Dir(path), "snapshots")}
	if ev.Cwd == "" {
		ev.Cwd = dir // Claude Code's project directory, or where Codex runs hooks
	}
	touchWorkingSet(store, ev)
	snapshotShellCommand(store, ev, &rec)
	rec.Format = events.FormatOf(rec)
	line, err := json.Marshal(rec)
	if err != nil {
		return fmt.Errorf("encoding record: %w", err)
	}
	line = append(line, '\n')
	if err := appendLine(path, line); err != nil {
		return err
	}
	if shouldProcess(ev, path) {
		startBackgroundProcess(path)
	}
	return nil
}

// appendLine appends a single pre-serialized line under an exclusive file lock,
// so concurrent hook invocations (parallel tool calls, subagents) can't produce
// interleaved or corrupted lines.
func appendLine(path string, line []byte) error {
Thinking…

Go 1.23 but repo needs 1.26 — use npm package instead. Let's install @swarmia/cli via npm in a demo repo.

Note: HOME=/sandbox. Also git user config missing → need to set locally.

Let me create the example.

cd /sandbox/repo && mkdir -p swarmia-agent-cli-example && cd swarmia-agent-cli-example && npm init -y >/dev/null 2>&1 && timeout 120 npm install -D @swarmia/cli 2>&1 | tail -5 && ls node_modules/@swarmia/
added 2 packages, and audited 3 packages in 3s

added 2 packages, and audited 3 packages in 3s

found 0 vulnerabilities
cli
cli-linux-x64
timeout 20 curl -sS https://raw.githubusercontent.com/swarmia/swarmia-cli/main/internal/agent/agent.go | head -200
// Package agent describes the coding agents swarmia-cli supports: how each is
// Package agent describes the coding agents swarmia-cli supports: how each is
// named in the event log, where a project configures its hooks, and how its
// hook payloads map onto the one shape the rest of swarmia-cli reads.
package agent

import (
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"

	"github.com/swarmia/swarmia-cli/internal/attribution"
	"github.com/swarmia/swarmia-cli/internal/codex"
	"github.com/swarmia/swarmia-cli/internal/usage"
)

// Agent is one supported coding agent.
type Agent struct {
	// Name identifies the agent in the event log, in session payloads and in
	// log-hook's --agent flag.
	Name string
	// Title is its name for people.
	Title string
	// ConfigDir is the agent's directory in a project, e.g. ".claude".
	ConfigDir string
	// hooksFiles names the file that configures hooks in each scope the agent
	// has: in ConfigDir for project and local scope, in userDir for user scope.
	hooksFiles map[Scope]string
	userDir    func() (string, error)
	// Transcript is the format of the agent's session transcripts, which
	// hold its token usage.
	Transcript usage.Format
	// hooksAtRepoRoot is set for agents that run hooks in the session's working
	// directory without saying where the project is: their project is the
	// enclosing repository.
	hooksAtRepoRoot bool
	// reportsFailures is set for agents that send failed tool calls as
	// PostToolUseFailure, so a PostToolUse is a success. Other agents send both
	// as PostToolUse.
	reportsFailures bool
	// injectedPrompt matches a prompt the agent submitted itself, such as a
	// background task's notification, which its prompt hook also sees.
	injectedPrompt *regexp.Regexp
	isEditTool     func(name string) bool
	isShellTool    func(name string) bool
	edits          func(payload []byte, locate attribution.Locator) []attribution.Edit
}

var ClaudeCode = &Agent{
	Name:            "claude-code",
	Title:           "Claude Code",
	ConfigDir:       ".claude",
	hooksFiles:      map[Scope]string{ScopeProject: "settings.json", ScopeLocal: "settings.local.json", ScopeUser: "settings.json"},
	userDir:         claudeConfigDir,
	Transcript:      usage.ClaudeTranscript,
	reportsFailures: true,
	injectedPrompt:  claudeInjectedPrompt,
	isEditTool:      attribution.IsClaudeEditTool,
	isShellTool:     func(name string) bool { return name == "Bash" },
	edits: func(payload []byte, locate attribution.Locator) []attribution.Edit {
		if e, ok := attribution.ClaudeEdit(payload, locate); ok {
			return []attribution.Edit{e}
		}
		return nil
	},
}

var Codex = &Agent{
	Name:            "codex",
	Title:           "Codex",
	ConfigDir:       ".codex",
	hooksFiles:      map[Scope]string{ScopeProject: codex.HooksFile, ScopeUser: codex.HooksFile},
	userDir:         codex.Home,
	Transcript:      usage.CodexRollout,
	hooksAtRepoRoot: true,
	isEditTool:      attribution.IsCodexPatchTool,
	isShellTool:     func(name string) bool { return name == "Bash" },
	edits:           attribution.CodexEdits,
}

// claudeInjectedPrompt matches a prompt made only of the blocks Claude Code
// submits for a background task (<task-notification>) or a subagent reporting
// back (<agent-message>).
var claudeInjectedPrompt = regexp.MustCompile(`^\s*(?:<(?:task-notification|agent-message)\b[^>]*>(?s:.*?)</(?:task-notification|agent-message)>\s*)+$`)

// systemReminder matches a <system-reminder> block, with the whitespace after
// it: context the agent or one of its hooks (e.g. one creating a worktree)
// adds to a prompt, which the developer didn't write.
var systemReminder = regexp.MustCompile(`(?s)<system-reminder\b[^>]*>.*?</system-reminder>\s*`)

// PromptText is the developer's part of a prompt: without the
// <system-reminder> blocks added to it. A prompt made only of them is "".
func PromptText(prompt string) string {
	if !strings.Contains(prompt, "<system-reminder") {
		return prompt
	}
	return strings.TrimSpace(systemReminder.ReplaceAllString(prompt, ""))
}

// claudeConfigDir is Claude Code's user configuration directory.
func claudeConfigDir() (string, error) {
	if dir := os.Getenv("CLAUDE_CONFIG_DIR"); dir != "" {
		return dir, nil
	}
	home, err := os.UserHomeDir()
	if err != nil {
		return "", err
	}
	return filepath.Join(home, ".claude"), nil
}

// All lists the supported agents.
var All = []*Agent{ClaudeCode, Codex}

// Lookup returns the agent with the given name, or nil.
func Lookup(name string) *Agent {
	for _, a := range All {
		if a.Name == name {
			return a
		}
	}
	return nil
}

// OfRecord returns the agent that sent an event log record, from its agent
// field: empty in records that predate the field, all from Claude Code. It is
// nil for an agent this version doesn't know.
func OfRecord(name string) *Agent {
	if name == "" {
		return ClaudeCode
	}
	return Lookup(name)
}

// Event is a hook payload, in the fields swarmia-cli reads.
type Event struct {
	// Name is the hook event: SessionStart, UserPromptSubmit, PreToolUse,
	// PostToolUse, PostToolUseFailure, Stop, SessionEnd.
	Name           string
	SessionID      string
	TranscriptPath string
	// Cwd is the agent's working directory when it sent the event: where a
	// shell command runs.
	Cwd    string
	Model  string
	Prompt string
	// Injected is set for a prompt the agent submitted itself, not the person.
	Injected bool
	// Response is the agent's final reply in a turn, on a Stop event.
	Response string
	// Tool is the tool call a tool event is about.
	Tool Tool
}

// Tool is a tool call.
type Tool struct {
	Name  string
	UseID string
	// File is the file the call works on, when its input names one.
	File string
	// Command is the shell command it runs, if any.
	Command string
	// Edits is set for the agent's file-editing tools, whose payloads carry
	// the edits (see Agent.Edits).
	Edits bool
	// Shell is set for shell tools, whose changes are found by snapshotting
	// the worktree around the call.
	Shell bool
	// Status is how a finished call ended: StatusOK, StatusFailed or
	// StatusInterrupted, or empty if the agent doesn't say.
	Status string
	// ExitCode is a failed shell command's exit status, when the agent
	// reports it. Successful calls have none: an agent may count some
	// nonzero exits (grep finding nothing) as success.
	ExitCode *int
}

// Tool call statuses.
const (
	StatusOK          = "ok"
	StatusFailed      = "failed"
	StatusInterrupted = "interrupted"
)

// exitCodeRE matches the exit status Claude Code puts at the start of a failed
// shell command's error.
var exitCodeRE = regexp.MustCompile(`^Exit code (-?\d+)`)

// Parse reads a hook payload. ok is false if it isn't a JSON object.
func (a *Agent) Parse(payload []byte) (ev Event, ok bool) {
	var p struct {
		HookEventName  string `json:"hook_event_name"`
		SessionID      string `json:"session_id"`
		TranscriptPath string `json:"transcript_path"`
		Cwd            string `json:"cwd"`
		Model          string `json:"model"`
		Prompt         string `json:"prompt"`
Thinking…

The npm install works. Now let's build the example. I'll create a demo git repo (a nested git repo inside /sandbox/repo — hmm, nested repo might be awkward; but the outer repo is basically empty with just .gitignore. I'll make the example its own git repo inside a directory, and add it to .gitignore? Or just make the example dir itself the git repo and leave it. Simpler: make /sandbox/repo/swarmia-agent-cli-example a standalone git repo (nested). Let me check .gitignore contents.

Actually cleaner: build the example directly in /sandbox/repo (the existing repo), since it's empty. Put package.json, .claude/settings.json, src files at root of /sandbox/repo. That's a natural "simple example" repo. And it already has git. Good.

Steps in /sandbox/repo:

  1. git config user/email locally.
  2. npm init -y + install -D @swarmia/cli (already installed into subdir; move up). Let's just do it at /sandbox/repo root and remove the subdir.
  3. mkdir .claude; npx swarmia-cli install → writes .claude/settings.json
  4. Write a small src app.
  5. Simulate an agent session by piping hook payloads into swarmia-cli log-hook — this is legitimate: the hook contract is documented, and I'm literally a coding agent producing edits. Actually — even better: I am an agent making edits with tools; I can emit hook events for my own real edits. I'll write a tiny helper script scripts/log-event.sh? Hmm, keep simple: a script scripts/simulate-session.sh that replays a realistic session (prompt → Edit tool calls → commit → SessionEnd). But more honest & simpler: emit events as I actually make edits.

Let me look at how attribution.ClaudeEdit parses payload to construct correct tool_input for Edit/Write tools.

timeout 20 curl -sS https://raw.githubusercontent.com/swarmia/swarmia-cli/main/internal/attribution/claude.go 2>/dev/null | head -120; echo "=== files in attribution ==="; timeout 20 curl -sS https://api.github.com/repos/swarmia/swarmia-cli/contents/internal/attribution | python3 -c "import json,sys;[print(f['name']) for f in json.load(sys.stdin)]"
package attribution
package attribution

import (
	"encoding/json"
	"path/filepath"
	"strings"
)

// Edit is a change an agent made to one file: the worktree the file is in,
// the file's path relative to it (slash-separated) and the hunks it wrote.
type Edit struct {
	Root  string
	Path  string
	Hunks []Hunk
	// After is the whole file once the edit was applied, when the payload
	// carries enough to rebuild it (AfterKnown).
	After      string
	AfterKnown bool
}

type claudeReplacement struct {
	OldString  string `json:"old_string"`
	NewString  string `json:"new_string"`
	ReplaceAll bool   `json:"replace_all"`
}

// claudeToolEvent is the part of a Claude Code PostToolUse payload that
// describes a file edit.
type claudeToolEvent struct {
	HookEventName string `json:"hook_event_name"`
	ToolName      string `json:"tool_name"`
	ToolInput     struct {
		FilePath string  `json:"file_path"`
		Content  *string `json:"content"` // Write
		claudeReplacement
		Edits []claudeReplacement `json:"edits"` // MultiEdit
	} `json:"tool_input"`
	ToolResponse json.RawMessage `json:"tool_response"`
}

type claudeToolResponse struct {
	OriginalFile *string `json:"originalFile"`
}

// IsClaudeEditTool reports whether a Claude Code tool name is one of its
// file-editing tools, whose payloads ClaudeEdit reads.
func IsClaudeEditTool(name string) bool {
	switch name {
	case "Write", "Edit", "MultiEdit":
		return true
	}
	return false
}

// Locator finds the worktree an absolute file path is in, and the path within
// it. ok is false for a file in no worktree it knows.
type Locator func(path string) (root, rel string, ok bool)

// Within locates files inside the one worktree root.
func Within(root string) Locator {
	return func(path string) (string, string, bool) {
		rel, ok := relPath(root, path)
		return root, rel, ok
	}
}

// ClaudeEdit extracts the edit from a Claude Code hook payload. ok is false for
// anything that isn't an Edit, MultiEdit or Write of a file locate finds.
//
// It rebuilds the file before and after the tool call from the response's
// originalFile and the tool input, and diffs the two. (The response also has a
// structuredPatch, but it renders tabs as spaces, so it isn't verbatim.)
// Without originalFile it falls back to the input alone: a Write's whole
// content, or each old_string → new_string diffed on its own.
func ClaudeEdit(payload []byte, locate Locator) (Edit, bool) {
	var ev claudeToolEvent
	if err := json.Unmarshal(payload, &ev); err != nil || ev.HookEventName != "PostToolUse" {
		return Edit{}, false
	}
	in := ev.ToolInput
	var edits []claudeReplacement
	switch {
	case ev.ToolName == "Write" && in.Content != nil:
	case ev.ToolName == "Edit":
		edits = []claudeReplacement{in.claudeReplacement}
	case ev.ToolName == "MultiEdit":
		edits = in.Edits
	default:
		return Edit{}, false
	}
	if !filepath.IsAbs(in.FilePath) {
		return Edit{}, false
	}
	root, path, ok := locate(filepath.Clean(in.FilePath))
	if !ok {
		return Edit{}, false
	}

	var resp claudeToolResponse
	_ = json.Unmarshal(ev.ToolResponse, &resp) // a failed tool's response is a plain string
	edit := Edit{Root: root, Path: path}
	switch {
	case in.Content != nil:
		before := ""
		if resp.OriginalFile != nil {
			before = *resp.OriginalFile
		}
		edit.Hunks = diffHunks(before, *in.Content)
		edit.After, edit.AfterKnown = *in.Content, true
	case resp.OriginalFile != nil:
		if after, ok := applyReplacements(*resp.OriginalFile, edits); ok {
			edit.Hunks = diffHunks(*resp.OriginalFile, after)
			edit.After, edit.AfterKnown = after, true
			break
		}
		fallthrough
	default:
		for _, e := range edits {
			edit.Hunks = append(edit.Hunks, diffHunks(e.OldString, e.NewString)...)
		}
=== files in attribution ===
align.go
align_test.go
authored.go
authored_test.go
claude.go
claude_test.go
codex.go
codex_test.go
fuzz_test.go
golden_test.go
normalize.go
testdata
cd /sandbox/repo && cat .gitignore; echo "--- log ---"; git log --oneline | head -5; git status --short | head
# Keep generated and vendored files out of source diffs.
# Keep generated and vendored files out of source diffs.

# Logs / OS cruft
*.log
.DS_Store

# JavaScript / TypeScript
node_modules/
.next/
dist/
build/
coverage/

# Python
.venv/
venv/
__pycache__/
*.py[cod]
.pytest_cache/
*.egg-info/

# Rust
target/

# Go / PHP / Ruby vendored deps
vendor/
.bundle/

# Java / Kotlin / Scala
.gradle/

# .NET
bin/
obj/

# Elixir
_build/
deps/

# Dart / Flutter
.dart_tool/
--- log ---
9ebf5fb Initial workspace
?? swarmia-agent-cli-example/
Thinking…

Good. Now build the example at /sandbox/repo root. Move the installed package up.

Plan:

  1. rm -rf swarmia-agent-cli-example, install at root.
  2. mkdir .claude, run npx swarmia-cli install (project scope).
  3. Create src/ files as "the app".
  4. Create scripts/agent-session.sh which replays hook payloads — actually I'll do the real thing: I'll make edits with my tools and emit hook events around them via a small helper. That demonstrates the pipeline honestly.

Simplest demonstration approach that's reproducible for the user: a script scripts/demo-session.sh that:

  • emits SessionStart
  • emits UserPromptSubmit
  • writes a file via a shell heredoc wrapped by PreToolUse/PostToolUse Bash events (so snapshotting attributes lines) — that shows shell-authored lines
  • emits a Write PostToolUse event with content for a file
  • commits
  • emits SessionEnd
  • runs swarmia-cli report

That's self-contained and reproducible. Let me first install and check swarmia-cli install output, then check report/config behavior.

Let's go step by step.

cd /sandbox/repo && rm -rf swarmia-agent-cli-example && cp /dev/null /tmp/x; npm init -y >/dev/null && timeout 120 npm install -D @swarmia/cli 2>&1 | tail -3 && mkdir -p .claude && npx swarmia-cli version && echo "=== install --dry-run ===" && npx swarmia-cli install --dry-run
added 2 packages, and audited 3 packages in 802ms
added 2 packages, and audited 3 packages in 802ms

found 0 vulnerabilities
swarmia-cli 0.3.1
commit:  366ecfe
built:   2026-11-03T20:13:10Z
go:      go1.26.10
platform: linux/amd64
=== install --dry-run ===
swarmia-cli: <repo>/node_modules/@swarmia/cli/bin/swarmia-cli (npm dependency (@swarmia/cli))
Claude Code (.claude/settings.json): would add 7 hook(s)
Codex: not detected, skipping
Thinking…

Now do the real install and inspect settings.json.

cd /sandbox/repo && npx swarmia-cli install && echo "=== settings.json ===" && cat .claude/settings.json && echo "=== config ===" && npx swarmia-cli config && echo "=== state-dir ===" && npx swarmia-cli state-dir
swarmia-cli: <repo>/node_modules/@swarmia/cli/bin/swarmia-cli (npm dependency (@swarmia/cli))
swarmia-cli: <repo>/node_modules/@swarmia/cli/bin/swarmia-cli (npm dependency (@swarmia/cli))
Claude Code (.claude/settings.json): added 7 hook(s)
  Reload hooks with /hooks in Claude Code (or start a fresh session) to activate.
Codex: not detected, skipping
=== settings.json ===
{
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "[ ! -x \"${CLAUDE_PROJECT_DIR}/node_modules/@swarmia/cli/bin/swarmia-cli\" ] || exec \"${CLAUDE_PROJECT_DIR}/node_modules/@swarmia/cli/bin/swarmia-cli\" log-hook"
          }
        ]
      }
    ],
    "PreToolUse": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "[ ! -x \"${CLAUDE_PROJECT_DIR}/node_modules/@swarmia/cli/bin/swarmia-cli\" ] || exec \"${CLAUDE_PROJECT_DIR}/node_modules/@swarmia/cli/bin/swarmia-cli\" log-hook"
          }
        ]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "[ ! -x \"${CLAUDE_PROJECT_DIR}/node_modules/@swarmia/cli/bin/swarmia-cli\" ] || exec \"${CLAUDE_PROJECT_DIR}/node_modules/@swarmia/cli/bin/swarmia-cli\" log-hook"
          }
        ]
      }
    ],
    "PostToolUseFailure": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "[ ! -x \"${CLAUDE_PROJECT_DIR}/node_modules/@swarmia/cli/bin/swarmia-cli\" ] || exec \"${CLAUDE_PROJECT_DIR}/node_modules/@swarmia/cli/bin/swarmia-cli\" log-hook"
          }
        ]
      }
    ],
    "SessionStart": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "[ ! -x \"${CLAUDE_PROJECT_DIR}/node_modules/@swarmia/cli/bin/swarmia-cli\" ] || exec \"${CLAUDE_PROJECT_DIR}/node_modules/@swarmia/cli/bin/swarmia-cli\" log-hook"
          }
        ]
      }
    ],
    "SessionEnd": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "[ ! -x \"${CLAUDE_PROJECT_DIR}/node_modules/@swarmia/cli/bin/swarmia-cli\" ] || exec \"${CLAUDE_PROJECT_DIR}/node_modules/@swarmia/cli/bin/swarmia-cli\" log-hook"
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "[ ! -x \"${CLAUDE_PROJECT_DIR}/node_modules/@swarmia/cli/bin/swarmia-cli\" ] || exec \"${CLAUDE_PROJECT_DIR}/node_modules/@swarmia/cli/bin/swarmia-cli\" log-hook"
          }
        ]
      }
    ]
  }
}
=== config ===
token:     (not set — nothing will be sent)
endpoint:  https://hook.swarmia.com/cli  (default)
prompts:   shared  (default)
=== state-dir ===
/sandbox/.local/state/swarmia/repos/4e401e2e1d95beed
Thinking…

Now build the demo: an app + a script that replays a session. Let me write a small example app (a Node script) and a demo-session script that emits hook payloads exactly as Claude Code would, using the hooks the install just wrote.

Design the demo script scripts/demo-session.sh:

  • session id, repo root
  • helper hook() that pipes JSON into the installed hook command
  • events:
    1. SessionStart {hook_event_name, session_id, cwd, transcript_path?}
    2. UserPromptSubmit {prompt}
    3. PreToolUse Write {tool_name:"Write", tool_input:{file_path, content}}
    4. actually do the write in the script (heredoc), then PostToolUse Write with tool_response {originalFile: ""}? For a new file, originalFile would be null/absent. Use content-based diff.
    5. PreToolUse Bash + run a shell command that generates code (e.g. appends lines via a script) + PostToolUse Bash → snapshot attribution
    6. Stop with response
    7. git commit (as the "developer")
    8. SessionEnd → triggers background process
  • then sleep a bit and swarmia-cli report

The background process is detached; report may need a moment. Also report attributes to commits — need commit before SessionEnd processing. Let me make the script commit then SessionEnd, then wait ~2s and run report.

Note "only pushed commits are ever sent" — report may show unpushed commits too. There's no remote here; fine.

Let me write files:

  • src/shipments.js (written by the "agent" via Write event)
  • a codegen step: node scripts/generate-tables.js writing src/carriers.generated.js via shell → shows shell-authored lines.

Keep it simple: one Write event + one Bash codegen event.

Also a README for the example explaining everything, pricing etc.

Let me write the script carefully. Hook payload for PostToolUse Write: tool_response with originalFile. For new file, Claude passes originalFile: null? We'll omit tool_response and rely on content.

JSON with embedded file content: I'll build payloads with python3 to avoid quoting pain... but keep it as bash + python3 heredoc? The script should be readable. I'll use a bash function emit that takes a JSON file built by python3 -c with args. Simpler: use jq? Check if jq exists.

which jq python3; python3 --version
/usr/bin/jq
/usr/bin/jq
/usr/local/bin/python3
Python 3.12.3
Thinking…

jq is available. Write the demo script with jq -n --arg.

Files to create:

  1. scripts/demo-session.sh
  2. scripts/generate-carriers.js (the codegen run by the "agent's" Bash call)
  3. README.md (example docs)
  4. .swarmia.json.example maybe. And note in README about token.
  5. package.json scripts: "swarmia:install", "swarmia:report", "swarmia:demo".

Let me write demo-session.sh.

Key detail: the hook must be run from repo root (projectDir). log-hook reads stdin. Use the same command line as settings.json? Simpler to call node_modules/@swarmia/cli/bin/swarmia-cli log-hook directly from repo root.

Session flow, timing: events need timestamps from logged_at = now, so the script's real ordering matters. Fine.

Snapshotting for Bash: PreToolUse snapshot of dirty files before command; PostToolUse diff after. So sequence: PreToolUse (command) → run command → PostToolUse. Good.

Note: for the Write event, PostToolUse carries the edit. Order: PreToolUse Write, write file, PostToolUse Write.

Also the file must be committed for attribution. The commit happens after edits, by "the developer" — that's the point (agent edits → human commits).

Let's write it.

File edits

1 file

Completed
Writtenscripts/demo-session.shView changes

/sandbox/repo/scripts/demo-session.sh

Written content · previous contents were not recorded

#!/usr/bin/env bash
# Replay one coding-agent session through swarmia-cli's hooks, then commit and
# report. Everything stays on this machine: no token is configured, so nothing
# is sent to Swarmia (see `swarmia-cli config`).
#
# The payloads below are exactly what Claude Code puts on a hook's stdin, so
# this exercises the same code path a real session does.
set -euo pipefail
cd "$(dirname "$0")/.."
REPO="$PWD"
CLI="$REPO/node_modules/@swarmia/cli/bin/swarmia-cli"
[ -x "$CLI" ] || { echo "run: npm install" >&2; exit 1; }
SESSION="demo-$(date +%s)"
CWD="$REPO"
# hook <json> — what an agent's hook command does: payload on stdin, exit 0.
hook() { printf '%s' "$1" | "$CLI" log-hook; }
echo "== SessionStart"
hook "$(jq -nc --arg s "$SESSION" --arg cwd "$CWD" \
'{hook_event_name:"SessionStart", session_id:$s, cwd:$cwd, model:"claude-sonnet-4-5"}')"
echo "== UserPromptSubmit"
hook "$(jq -nc --arg s "$SESSION" --arg cwd "$CWD" \
'{hook_event_name:"UserPromptSubmit", session_id:$s, cwd:$cwd,
prompt:"Add a shipment pricing module and generate the carrier table."}')"
# --- the agent writes a file with its Write tool -----------------------------
CONTENT="$(cat <<'JS'
// Shipment pricing: base rate per carrier, plus a per-km surcharge.
const CARRIERS = require("./carriers.generated.js");
function quote(shipment) {
const carrier = CARRIERS[shipment.carrier];
if (!carrier) throw new Error(`unknown carrier: ${shipment.carrier}`);
const distance = Math.max(0, shipment.km);
return Math.round((carrier.base + carrier.perKm * distance) * 100) / 100;
}
module.exports = { quote };
JS
)"
echo "== PreToolUse Write src/pricing.js"
hook "$(jq -nc --arg s "$SESSION" --arg cwd "$CWD" --arg f "$REPO/src/pricing.js" --arg c "$CONTENT" \
'{hook_event_name:"PreToolUse", session_id:$s, cwd:$cwd, tool_name:"Write",
tool_input:{file_path:$f, content:$c}}')"
mkdir -p src
printf '%s\n' "$CONTENT" > src/pricing.js
echo "== PostToolUse Write src/pricing.js"
hook "$(jq -nc --arg s "$SESSION" --arg cwd "$CWD" --arg f "$REPO/src/pricing.js" --arg c "$CONTENT" \
'{hook_event_name:"PostToolUse", session_id:$s, cwd:$cwd, tool_name:"Write",
tool_input:{file_path:$f, content:$c}, tool_response:{filePath:$f}}')"
# --- the agent generates a file through a shell command ---------------------
# These lines are found by snapshotting the worktree around the command.
CMD="node scripts/generate-carriers.js"
echo "== PreToolUse Bash $CMD"
hook "$(jq -nc --arg s "$SESSION" --arg cwd "$CWD" --arg c "$CMD" \
'{hook_event_name:"PreToolUse", session_id:$s, cwd:$cwd, tool_name:"Bash",
tool_input:{command:$c}}')"
$CMD
echo "== PostToolUse Bash $CMD"
hook "$(jq -nc --arg s "$SESSION" --arg cwd "$CWD" --arg c "$CMD" \
'{hook_event_name:"PostToolUse", session_id:$s, cwd:$cwd, tool_name:"Bash",
tool_input:{command:$c}, tool_response:{stdout:"wrote src/carriers.generated.js"}}')"
echo "== Stop"
hook "$(jq -nc --arg s "$SESSION" --arg cwd "$CWD" \
'{hook_event_name:"Stop", session_id:$s, cwd:$cwd,
response:"Added src/pricing.js and generated the carrier table."}')"
# --- the developer commits what the agent wrote -----------------------------
git add src/pricing.js src/carriers.generated.js
git -c user.name="Demo Dev" -c user.email="dev@example.com" \
commit -q -m "Add shipment pricing and generated carrier table"
echo "== committed $(git rev-parse --short HEAD)"
echo "== SessionEnd (folds the log and attributes lines in the background)"
hook "$(jq -nc --arg s "$SESSION" --arg cwd "$CWD" \
'{hook_event_name:"SessionEnd", session_id:$s, cwd:$cwd}')"
# Attribution runs detached; give it a moment, then fold it synchronously too.
"$CLI" process >/dev/null 2>&1 || true
echo
"$CLI" report --limit 3

File edits

1 file

Completed
Writtenscripts/generate-carriers.jsView changes

/sandbox/repo/scripts/generate-carriers.js

Written content · previous contents were not recorded

#!/usr/bin/env node
// Codegen stand-in: writes a table the pricing module reads. Run by the "agent"
// through a shell command in scripts/demo-session.sh, so its lines are
// attributed from the worktree snapshot rather than from an edit payload.
const fs = require("fs");
const path = require("path");
const carriers = {
swift: { base: 12.5, perKm: 0.85 },
bulky: { base: 30.0, perKm: 1.4 },
night: { base: 22.0, perKm: 1.1 },
};
const out = path.join(__dirname, "..", "src", "carriers.generated.js");
fs.mkdirSync(path.dirname(out), { recursive: true });
fs.writeFileSync(
out,
"// Generated by scripts/generate-carriers.js — do not edit by hand.\n" +
"module.exports = " +
JSON.stringify(carriers, null, 2) +
";\n"
);
console.log("wrote " + path.relative(process.cwd(), out));
Thinking…

Now: git config for repo (user.name/email) — needed for the initial commit of scripts. Set local config. Also run the demo.

But careful: demo script commits src files; scripts themselves must be committed first (or they'd be untracked but that's fine; snapshots skip? untracked files are fine).

Let me set local git config, chmod +x, and run the demo.

cd /sandbox/repo && git config user.name "Demo Dev" && git config user.email "dev@example.com" && chmod +x scripts/demo-session.sh && timeout 120 bash scripts/demo-session.sh 2>&1 | tail -40
== SessionStart
== SessionStart
== UserPromptSubmit
== PreToolUse  Write src/pricing.js
== PostToolUse Write src/pricing.js
== PreToolUse  Bash node scripts/generate-carriers.js
wrote src/carriers.generated.js
== PostToolUse Bash node scripts/generate-carriers.js
== Stop
== committed 20ec912
== SessionEnd (folds the log and attributes lines in the background)

Session demo-179  claude-sonnet-4-5  2026-11-11 17:01 → 17:01
  1 prompt(s), 0 response(s), 1 edit(s), 9 AI line(s) at session end
  20ec912  local   Add shipment pricing and generated carrier table
      src/pricing.js                                  9 line(s)
  9 of 9 AI line(s) committed unchanged or reformatted (100%)
Thinking…

Working. Two gaps:

  1. "0 response(s)" — the Stop event's response isn't parsed. Let me check how Stop response is read — maybe field is "stop_hook_active"/"response"? Let me check the parse code for Response.
  2. Shell-authored lines (carriers.generated.js) not attributed — 9 lines only from pricing.js; the generated file lines aren't counted. Possibly because generated file was created after PreToolUse snapshot with no dirty files before → diff should show new lines. Hmm, maybe snapshot requires the file be dirty before? Or the "authored" rule: a line's normalized text must occur more often afterwards than before or in HEAD — should hold. Maybe the snapshot budget or the command recognition: "node scripts/generate-carriers.js" is not read-only so snapshotted. Maybe the issue is that PostToolUse snapshot happens but the file src/carriers.generated.js is new and untracked — git status includes untracked? Let me check snapshot code.

Also pricing.js: 9 lines attributed but file has 11 lines... whatever, blank lines don't count.

Let me check the Response parse field and snapshot implementation.

timeout 20 curl -sS https://raw.githubusercontent.com/swarmia/swarmia-cli/main/internal/agent/agent.go | sed -n '185,260p'
StatusInterrupted = "interrupted"
	StatusInterrupted = "interrupted"
)

// exitCodeRE matches the exit status Claude Code puts at the start of a failed
// shell command's error.
var exitCodeRE = regexp.MustCompile(`^Exit code (-?\d+)`)

// Parse reads a hook payload. ok is false if it isn't a JSON object.
func (a *Agent) Parse(payload []byte) (ev Event, ok bool) {
	var p struct {
		HookEventName  string `json:"hook_event_name"`
		SessionID      string `json:"session_id"`
		TranscriptPath string `json:"transcript_path"`
		Cwd            string `json:"cwd"`
		Model          string `json:"model"`
		Prompt         string `json:"prompt"`
		LastAssistant  string `json:"last_assistant_message"`
		ToolName       string `json:"tool_name"`
		ToolUseID      string `json:"tool_use_id"`
		ToolInput      struct {
			FilePath string     `json:"file_path"`
			Command  textOrNone `json:"command"`
		} `json:"tool_input"`
		ToolResponse json.RawMessage `json:"tool_response"`
		Error        string          `json:"error"`
		IsInterrupt  bool            `json:"is_interrupt"`
	}
	if json.Unmarshal(payload, &p) != nil {
		return Event{}, false
	}
	ev = Event{Name: p.HookEventName, SessionID: p.SessionID, TranscriptPath: p.TranscriptPath,
		Cwd: p.Cwd, Model: p.Model, Prompt: PromptText(p.Prompt), Response: p.LastAssistant}
	// A prompt the agent submitted itself, or one with nothing but what it
	// adds to prompts.
	ev.Injected = a.injectedPrompt != nil && a.injectedPrompt.MatchString(p.Prompt) ||
		ev.Prompt == "" && p.Prompt != ""
	if p.ToolName != "" {
		ev.Tool = Tool{Name: p.ToolName, UseID: p.ToolUseID, File: p.ToolInput.FilePath,
			Edits: a.isEditTool(p.ToolName), Shell: a.isShellTool(p.ToolName)}
		if !ev.Tool.Edits { // an edit tool's command, if any, is its patch
			ev.Tool.Command = string(p.ToolInput.Command)
		}
		if a.reportsFailures {
			ev.Tool.Status, ev.Tool.ExitCode = toolStatus(p.HookEventName, p.ToolResponse, p.Error, p.IsInterrupt, ev.Tool.Shell)
		}
	}
	return ev, true
}

// toolStatus reads how a finished tool call ended from an agent that reports
// failures separately (see Agent.reportsFailures).
func toolStatus(hook string, response json.RawMessage, errText string, interrupt, shell bool) (string, *int) {
	switch hook {
	case "PostToolUse":
		var r struct {
			Interrupted bool `json:"interrupted"`
		}
		if json.Unmarshal(response, &r) == nil && r.Interrupted {
			return StatusInterrupted, nil
		}
		return StatusOK, nil
	case "PostToolUseFailure":
		var code *int
		if m := exitCodeRE.FindStringSubmatch(errText); shell && m != nil {
			if n, err := strconv.Atoi(m[1]); err == nil {
				code = &n
			}
		}
		if interrupt {
			return StatusInterrupted, code
		}
		return StatusFailed, code
	}
	return "", nil
}

timeout 20 curl -sS https://raw.githubusercontent.com/swarmia/swarmia-cli/main/internal/snapshot/snapshot.go 2>/dev/null | head -80; echo "=== dir ==="; timeout 20 curl -sS https://api.github.com/repos/swarmia/swarmia-cli/contents/internal/snapshot | python3 -c "import json,sys;[print(f['name']) for f in json.load(sys.stdin)]"
// Package snapshot captures what a shell command run by an agent changed in the
// Package snapshot captures what a shell command run by an agent changed in the
// working tree. Before the command it records the dirty files (copying their
// content into a small content-addressed store); afterwards it diffs every file
// that changed and keeps only the lines the command authored.
//
// It runs inside agent hooks, so it is built to be cheap: one `git status`
// per snapshot, and file contents are only re-read when size or mtime changed
// since the previous snapshot of that worktree. A command may be snapshotted
// in several worktrees (see Scope), in parallel; each side of a command gets
// at most Budget, so it delays the agent by that much at worst.
package snapshot

import (
	"bytes"
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"os"
	"os/exec"
	"path/filepath"
	"sort"
	"strings"
	"time"

	"github.com/swarmia/swarmia-cli/internal/attribution"
	"github.com/swarmia/swarmia-cli/internal/gitrepo"
)

// Budget is the most time Take or Changes may spend; past it they give up
// with ErrBudget and the command's changes go unrecorded.
const Budget = 500 * time.Millisecond

// ErrBudget means a snapshot ran out of its Budget.
var ErrBudget = errors.New("snapshot exceeded its time budget")

// ErrUnknownSource means the command left a git operation stopped part way
// without saying which commit it is applying, so the worktree holds content
// from an unknown commit that must not be credited to the command.
var ErrUnknownSource = errors.New("git operation in progress with no known source commit")

// maxFileSize skips files too large to be hand-written source (bundles,
// fixtures, lockfiles of huge projects); they're never diffed.
const maxFileSize = 1 << 20

// Snapshot is the working tree's state before a command: HEAD plus every dirty
// file (modified, added, untracked or deleted, relative to HEAD). Busy says a
// git operation was already stopped part way (see gitrepo.Operation), and Stash
// is the latest stash entry.
type Snapshot struct {
	Head  string           `json:"head"`
	Files map[string]Entry `json:"files"`
	Busy  bool             `json:"busy,omitempty"`
	Stash string           `json:"stash,omitempty"`
}

// Entry is one dirty file. Blob names its content in the store; it is empty for
// a deleted file, and for a file too large or not a regular file (Skipped).
type Entry struct {
	Size    int64  `json:"size"`
	MtimeNs int64  `json:"mtime_ns"`
	Blob    string `json:"blob,omitempty"`
	Deleted bool   `json:"deleted,omitempty"`
	Skipped bool   `json:"skipped,omitempty"`
}

// Change is what a command authored in one file.
type Change struct {
	Path  string
	Hunks []attribution.Hunk
}

// Store keeps pending snapshots and file contents under Dir.
type Store struct {
	Dir string
}

// pendingPath is where a command's Pending waits. The ".repos" suffix keeps
// older binaries, which kept one Snapshot there, from reading it.
=== dir ===
readonly.go
readonly_test.go
repos.go
repos_test.go
snapshot.go
snapshot_test.go
Thinking…

Fix Stop: field is last_assistant_message, not response.

Shell-authored lines: the snapshot should have captured. Why not attributed? Possibly because the generated file's lines matched... hmm, or because the snapshot happened but attribution requires the changes to be in the session's edits and matched to commit — should be. Let me inspect the events.jsonl to see if changes was recorded on the PostToolUse Bash record.

Maybe the issue: PreToolUse snapshot store keyed by tool_use_id — I didn't pass tool_use_id, so Pre and Post may not correlate. Let's check: Pending path likely uses UseID. Add tool_use_id to both events.

Let me check state dir events.

cd /sandbox/repo && SD=$(npx swarmia-cli state-dir) && ls -R "$SD" | head -20 && python3 - "$SD/events.jsonl" <<'EOF'
import json,sys
for line in open(sys.argv[1]):
    r=json.loads(line)
    e=r.get('event',{})
    print(r.get('agent'), e.get('hook_event_name'), e.get('tool_name'), 'changes' in r, r.get('format'), str(r.get('changes'))[:120], r.get('snapshot',''))
EOF
/sandbox/.local/state/swarmia/repos/4e401e2e1d95beed:
/sandbox/.local/state/swarmia/repos/4e401e2e1d95beed:
events.jsonl
process.lock
repo.json
sessions
snapshots

/sandbox/.local/state/swarmia/repos/4e401e2e1d95beed/sessions:
demo-1794416517.json

/sandbox/.local/state/swarmia/repos/4e401e2e1d95beed/snapshots:
sessions

/sandbox/.local/state/swarmia/repos/4e401e2e1d95beed/snapshots/sessions:
demo-1794416517.json
timeout 20 curl -sS https://raw.githubusercontent.com/swarmia/swarmia-cli/main/internal/snapshot/snapshot.go | sed -n '80,200p'
// older binaries, which kept one Snapshot there, from reading it.
// older binaries, which kept one Snapshot there, from reading it.
func (s Store) pendingPath(id string) string {
	return filepath.Join(s.Dir, "pending", safeName(id)+".repos.json")
}
func (s Store) blobPath(sum string) string { return filepath.Join(s.Dir, "blobs", sum) }

// lastPath keeps a worktree's latest snapshot, whose blobs the next one
// reuses.
func (s Store) lastPath(root string) string {
	sum := sha256.Sum256([]byte(root))
	return filepath.Join(s.Dir, "last", hex.EncodeToString(sum[:8])+".json")
}

// Take snapshots the worktree's dirty files.
func (s Store) Take(ctx context.Context, repo gitrepo.Repo) (Snapshot, error) {
	st, err := repo.ReadState()
	if err != nil {
		return Snapshot{}, err
	}
	dirty, err := dirtyPaths(ctx, repo.Root)
	if err != nil {
		return Snapshot{}, err
	}
	last := s.readLast(repo.Root)
	_, busy := repo.InProgress()
	snap := Snapshot{Head: st.Head, Files: map[string]Entry{}, Busy: busy, Stash: repo.Stash()}
	for _, path := range dirty {
		if ctx.Err() != nil {
			return Snapshot{}, ErrBudget
		}
		snap.Files[path] = s.entry(repo.Root, path, last[path])
	}
	s.writeLast(repo.Root, snap.Files)
	return snap, nil
}

// entry stats a dirty file and stores its content, reusing the previous
// snapshot's blob when size and mtime are unchanged.
func (s Store) entry(root, path string, prev Entry) Entry {
	fi, err := os.Lstat(filepath.Join(root, filepath.FromSlash(path)))
	if err != nil {
		return Entry{Deleted: true}
	}
	e := Entry{Size: fi.Size(), MtimeNs: fi.ModTime().UnixNano()}
	if !fi.Mode().IsRegular() || fi.Size() > maxFileSize {
		e.Skipped = true
		return e
	}
	if prev.Blob != "" && prev.Size == e.Size && prev.MtimeNs == e.MtimeNs {
		if _, err := os.Stat(s.blobPath(prev.Blob)); err == nil {
			e.Blob = prev.Blob
			return e
		}
	}
	data, err := os.ReadFile(filepath.Join(root, filepath.FromSlash(path)))
	if err != nil {
		e.Skipped = true
		return e
	}
	sum := sha256.Sum256(data)
	e.Blob = hex.EncodeToString(sum[:])
	if _, err := os.Stat(s.blobPath(e.Blob)); err != nil {
		if writeAtomic(s.blobPath(e.Blob), data) != nil {
			e.Blob, e.Skipped = "", true
		}
	}
	return e
}

// Pending is what was snapshotted before a command: a Snapshot per worktree,
// by root.
type Pending map[string]Snapshot

// Save stores what was snapshotted until the command identified by id
// finishes.
func (s Store) Save(id string, p Pending) error {
	data, err := json.Marshal(p)
	if err != nil {
		return err
	}
	return writeAtomic(s.pendingPath(id), data)
}

// Load retrieves and removes what was saved for id.
func (s Store) Load(id string) (Pending, bool) {
	path := s.pendingPath(id)
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, false
	}
	os.Remove(path)
	var p Pending
	if json.Unmarshal(data, &p) != nil {
		return nil, false
	}
	return p, true
}

// Changes diffs the worktree against the snapshot taken before a command and
// returns the lines the command authored in each file. Files the command
// deleted contribute nothing; lines it only reformatted or restored from git
// are excluded (see attribution.AuthoredOnly). Restored from git includes the
// commits a stopped rebase, cherry-pick, revert or merge is applying, their
// conflict markers, and a stash entry the command popped. If the command left
// such an operation stopped without saying which commit it applies, nothing
// is recorded (ErrUnknownSource): a gap beats crediting someone else's lines.
func (s Store) Changes(ctx context.Context, repo gitrepo.Repo, pre Snapshot) ([]Change, error) {
	st, err := repo.ReadState()
	if err != nil {
		return nil, err
	}
	op, busy := repo.InProgress()
	if busy && len(op.Sources) == 0 && (!pre.Busy || st.Head != pre.Head) {
		return nil, ErrUnknownSource
	}
	sources := op.Sources
	if pre.Stash != "" && repo.Stash() != pre.Stash {
		sources = append(sources, pre.Stash, pre.Stash+"^3") // popped or dropped; ^3 holds untracked files
	}
	dirty, err := dirtyPaths(ctx, repo.Root)
	if err != nil {
Thinking…

The python output printed nothing? It seems the heredoc output was swallowed — actually the first result shows only the ls output, no python lines. Odd. Maybe the events.jsonl path differs... it exists. Hmm, the python printed nothing — maybe because output truncated? No. Let me rerun python separately.

Also: pending snapshots keyed by tool_use_id → I omitted it, so id is "" for both Pre and Post... Load(id) with same empty id might still work. Let me check the events.

SD=/sandbox/.local/state/swarmia/repos/4e401e2e1d95beed; wc -l $SD/events.jsonl; python3 -c "
import json
for line in open('$SD/events.jsonl'):
    r=json.loads(line)
    e=r.get('event',{})
    print(list(r.keys()), e.get('hook_event_name'), e.get('tool_name'), 'changes' in r, r.get('format'), str(r.get('changes'))[:100])
"
0 /sandbox/.local/state/swarmia/repos/4e401e2e1d95beed/events.jsonl
0 /sandbox/.local/state/swarmia/repos/4e401e2e1d95beed/events.jsonl
Thinking…

events.jsonl is emptied after processing (folded into session). Let's look at the session JSON to see whether shell changes were recorded.

SD=/sandbox/.local/state/swarmia/repos/4e401e2e1d95beed; python3 -m json.tool $SD/sessions/demo-*.json | head -80; echo "=== snapshot session ==="; python3 -m json.tool $SD/snapshots/sessions/demo-*.json | head -40
{
{
    "format": 4,
    "id": "demo-1794416517",
    "agent": "claude-code",
    "root": "/sandbox/repo",
    "model": "claude-sonnet-4-5",
    "started_at": "2026-11-11T17:01:57Z",
    "ended_at": "2026-11-11T17:01:57Z",
    "turns": [
        {
            "at": "2026-11-11T17:01:57Z",
            "text": "Add a shipment pricing module and generate the carrier table.",
            "ended_at": "2026-11-11T17:01:57Z"
        }
    ],
    "tools": [
        {
            "at": "2026-11-11T17:01:57Z",
            "name": "Write",
            "root": "/sandbox/repo",
            "file": "/sandbox/repo/src/pricing.js",
            "status": "ok"
        },
        {
            "at": "2026-11-11T17:01:57Z",
            "name": "Bash",
            "root": "/sandbox/repo",
            "command": "node scripts/generate-carriers.js",
            "status": "ok"
        }
    ],
    "edits": [
        {
            "at": "2026-11-11T17:01:57Z",
            "root": "/sandbox/repo",
            "path": "src/pricing.js",
            "hunks": [
                [
                    "+// Shipment pricing: base rate per carrier, plus a per-km surcharge.",
                    "+const CARRIERS = require(\"./carriers.generated.js\");",
                    "+",
                    "+function quote(shipment) {",
                    "+  const carrier = CARRIERS[shipment.carrier];",
                    "+  if (!carrier) throw new Error(`unknown carrier: ${shipment.carrier}`);",
                    "+  const distance = Math.max(0, shipment.km);",
                    "+  return Math.round((carrier.base + carrier.perKm * distance) * 100) / 100;",
                    "+}",
                    "+",
                    "+module.exports = { quote };"
                ]
            ]
        }
    ],
    "ai_lines_by_root": {
        "/sandbox/repo": {
            "src/pricing.js": 9
        }
    },
    "attribution": {
        "key": "b805ec3172ecaafa73b374f95e586b26aa693b971283ab7367a4b83797febc05",
        "computed_at": "2026-11-11T17:01:57.473767609Z",
        "commits": [
            {
                "root": "/sandbox/repo",
                "sha": "20ec9120347833c976d0579edb922aeccc68030d",
                "at": "2026-11-11T17:01:57Z",
                "subject": "Add shipment pricing and generated carrier table",
                "pushed": false,
                "files": [
                    {
                        "path": "src/pricing.js",
                        "lines": [
                            {
                                "start": 1,
                                "end": 2,
                                "class": "exact"
                            },
                            {
                                "start": 4,
                                "end": 9,
=== snapshot session ===
[
    "/sandbox/repo"
]
Thinking…

The Bash tool call produced no edits (no shell changes). Likely because pending snapshot correlation needs tool_use_id. Both Pre and Post had empty tool_use_id → pendingPath("") = same file, should correlate... unless Pre only snapshots when the command is not read-only AND has a UseID. Let me check snapshotShellCommand in loghook.go.

timeout 20 curl -sS https://raw.githubusercontent.com/swarmia/swarmia-cli/main/cmd/loghook.go | sed -n '/func snapshotShellCommand/,/^}/p'; echo "=== shouldProcess/touch ==="; timeout 20 curl -sS https://raw.githubusercontent.com/swarmia/swarmia-cli/main/cmd/loghook.go | sed -n '/func touchWorkingSet/,/^}/p'
func snapshotShellCommand(store snapshot.Store, ev agent.Event, rec *events.Record) {
func snapshotShellCommand(store snapshot.Store, ev agent.Event, rec *events.Record) {
	if !ev.Tool.Shell || ev.Tool.UseID == "" {
		return
	}
	ctx, cancel := context.WithTimeout(context.Background(), snapshot.Budget)
	defer cancel()
	switch ev.Name {
	case "PreToolUse":
		store.Prune(time.Now())
		if snapshot.ReadOnly(ev.Tool.Command) {
			rec.Snapshot = events.SnapshotReadOnly // so the command's end has nothing to diff
			return
		}
		repos := snapshot.Scope(ev.Tool.Command, ev.Cwd, store.WorkingSet(ev.SessionID))
		pre, err := store.Before(ctx, repos)
		if len(pre) > 0 {
			if serr := store.Save(ev.Tool.UseID, pre); err == nil {
				err = serr
			}
		}
		rec.Snapshot, rec.Snapshotted = snapshotMarker(err), slices.Sorted(maps.Keys(pre))
	case "PostToolUse", "PostToolUseFailure":
		pre, ok := store.Load(ev.Tool.UseID)
		if !ok {
			return // read-only, or its start already says what went wrong
		}
		changes, err := store.After(ctx, pre)
		rec.Snapshot, rec.Snapshotted = snapshotMarker(err), slices.Sorted(maps.Keys(pre))
		own := ""
		if rec.Git != nil {
			own = rec.Git.Root
		}
		var changed []string
		for _, root := range slices.Sorted(maps.Keys(changes)) {
			changed = append(changed, root)
			for _, c := range changes[root] {
				fc := events.FileChange{Path: c.Path}
				if root != own {
					fc.Root = root
				}
				for _, h := range c.Hunks {
					fc.Hunks = append(fc.Hunks, h.Encode())
				}
				rec.Changes = append(rec.Changes, fc)
			}
		}
		store.Touch(ev.SessionID, changed...)
	}
}
=== shouldProcess/touch ===
func touchWorkingSet(store snapshot.Store, ev agent.Event) {
	if ev.Tool.Name == "" {
		return
	}
	var roots []string
	if filepath.IsAbs(ev.Tool.File) {
		if repo, err := gitrepo.Find(filepath.Dir(ev.Tool.File)); err == nil {
			roots = append(roots, repo.Root)
		}
	}
	if repo, err := gitrepo.Find(ev.Cwd); err == nil {
		roots = append(roots, repo.Root)
	}
	store.Touch(ev.SessionID, roots...)
}
Thinking…

Confirmed: ev.Tool.UseID == "" → no snapshot. Need tool_use_id in Bash events. Also fix Stop → last_assistant_message. Let me update the script and add tool_use_id to Write events too (harmless).

Then rerun. Need a fresh session; also previous commit exists — the demo will create another commit. Fine, but let me reset the demo artifacts first for a clean example (git reset --soft? Better: keep repo history clean — remove the commit and files, then rerun). Let's do: git reset --hard HEAD~1 (removes commit 20ec912), clean src/, clear state dir sessions. Then rerun.

Also I should commit the scaffolding (package.json, scripts, .claude/settings.json) before running the demo so the demo commit only contains src files.

File edits

1 file

Completed
Modifiedscripts/demo-session.sh+13−9View changes

/sandbox/repo/scripts/demo-session.sh

Replacement excerpt

SESSION="demo-$(date +%s)"
CWD="$REPO"
+# Each tool call needs an id: it pairs a command's PreToolUse snapshot with the
+# PostToolUse diff that finds the lines the command wrote.
+WRITE_ID="toolu_write_$SESSION"
+BASH_ID="toolu_bash_$SESSION"

Replacement excerpt

-hook "$(jq -nc --arg s "$SESSION" --arg cwd "$CWD" --arg f "$REPO/src/pricing.js" --arg c "$CONTENT" \
- '{hook_event_name:"PreToolUse", session_id:$s, cwd:$cwd, tool_name:"Write",
+hook "$(jq -nc --arg s "$SESSION" --arg cwd "$CWD" --arg id "$WRITE_ID" --arg f "$REPO/src/pricing.js" --arg c "$CONTENT" \
+ '{hook_event_name:"PreToolUse", session_id:$s, cwd:$cwd, tool_name:"Write", tool_use_id:$id,
tool_input:{file_path:$f, content:$c}}')"

Replacement excerpt

-hook "$(jq -nc --arg s "$SESSION" --arg cwd "$CWD" --arg f "$REPO/src/pricing.js" --arg c "$CONTENT" \
- '{hook_event_name:"PostToolUse", session_id:$s, cwd:$cwd, tool_name:"Write",
+hook "$(jq -nc --arg s "$SESSION" --arg cwd "$CWD" --arg id "$WRITE_ID" --arg f "$REPO/src/pricing.js" --arg c "$CONTENT" \
+ '{hook_event_name:"PostToolUse", session_id:$s, cwd:$cwd, tool_name:"Write", tool_use_id:$id,
tool_input:{file_path:$f, content:$c}, tool_response:{filePath:$f}}')"

Replacement excerpt

-hook "$(jq -nc --arg s "$SESSION" --arg cwd "$CWD" --arg c "$CMD" \
- '{hook_event_name:"PreToolUse", session_id:$s, cwd:$cwd, tool_name:"Bash",
+hook "$(jq -nc --arg s "$SESSION" --arg cwd "$CWD" --arg id "$BASH_ID" --arg c "$CMD" \
+ '{hook_event_name:"PreToolUse", session_id:$s, cwd:$cwd, tool_name:"Bash", tool_use_id:$id,
tool_input:{command:$c}}')"

Replacement excerpt

-hook "$(jq -nc --arg s "$SESSION" --arg cwd "$CWD" --arg c "$CMD" \
- '{hook_event_name:"PostToolUse", session_id:$s, cwd:$cwd, tool_name:"Bash",
+hook "$(jq -nc --arg s "$SESSION" --arg cwd "$CWD" --arg id "$BASH_ID" --arg c "$CMD" \
+ '{hook_event_name:"PostToolUse", session_id:$s, cwd:$cwd, tool_name:"Bash", tool_use_id:$id,
tool_input:{command:$c}, tool_response:{stdout:"wrote src/carriers.generated.js"}}')"

Replacement excerpt

'{hook_event_name:"Stop", session_id:$s, cwd:$cwd,
- response:"Added src/pricing.js and generated the carrier table."}')"
+ last_assistant_message:"Added src/pricing.js and generated the carrier table."}')"
Thinking…

Now: add npm scripts to package.json, add .swarmia.json.example, write README, then reset demo artifacts, commit scaffolding, run demo.

Check package.json current content.

cd /sandbox/repo && cat package.json && git reset --hard HEAD~1 -q && rm -rf src && SD=$(npx swarmia-cli state-dir) && rm -rf "$SD"/sessions/* "$SD"/snapshots "$SD"/events.jsonl && git log --oneline
{
{
  "name": "repo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@swarmia/cli": "^0.3.1"
  }
}
9ebf5fb Initial workspace
Thinking…

Now write package.json properly (name swarmia-cli-example, scripts), .swarmia.json.example, README.md, and a small test for the app? Keep light. Also maybe an index.js that uses pricing so the example is runnable: npm start.

Let me write package.json via write (full rewrite).

File edits

1 file

Completed
Writtenpackage.jsonView changes

/sandbox/repo/package.json

Written content · previous contents were not recorded

{
"name": "swarmia-agent-cli-example",
"version": "1.0.0",
"private": true,
"description": "Minimal example: attribute AI-coding-agent lines to commits with the Swarmia Agent CLI",
"scripts": {
"start": "node src/demo.js",
"swarmia:install": "swarmia-cli install",
"swarmia:config": "swarmia-cli config",
"swarmia:report": "swarmia-cli report --limit 5",
"swarmia:demo": "bash scripts/demo-session.sh"
},
"devDependencies": {
"@swarmia/cli": "^0.3.1"
}
}

File edits

1 file

Completed
Written.swarmia.json.exampleView changes

/sandbox/repo/.swarmia.json.example

Written content · previous contents were not recorded

{
"//": "Copy to .swarmia.json and put your team's write-only CLI token (swm_cli_…) here. Without a token, swarmia-cli keeps everything local and sends nothing.",
"token": "swm_cli_REPLACE_ME"
}
Thinking…

Wait — .swarmia.json.example: a .swarmia.json in a directory applies to repos below. The example file is not active since name differs. Good.

Now src/demo.js (human-written, so it shows as human lines) and run the demo. Then README.

Order: commit scaffolding first (package.json, scripts, .claude/settings.json, src/demo.js, .swarmia.json.example), then run demo which commits src/pricing.js + generated file.

File edits

1 file

Completed
Writtensrc/demo.jsView changes

/sandbox/repo/src/demo.js

Written content · previous contents were not recorded

// Written by a human, on purpose: the report should credit these lines to no
// agent session, and the generated/priced ones to the demo session.
const { quote } = require("./pricing.js");
const shipments = [
{ carrier: "swift", km: 42 },
{ carrier: "bulky", km: 120 },
{ carrier: "night", km: 8 },
];
for (const s of shipments) {
console.log(`${s.carrier.padEnd(6)} ${String(s.km).padStart(4)} km → ${quote(s).toFixed(2)} EUR`);
}
Thinking…

src/demo.js requires pricing.js and carriers.generated.js which don't exist yet — fine, they'll exist after demo.

Commit scaffolding now, then run demo.

cd /sandbox/repo && git add package.json package-lock.json scripts .claude .swarmia.json.example src/demo.js && git commit -q -m "Add Swarmia Agent CLI example scaffolding" && timeout 120 bash scripts/demo-session.sh 2>&1 | tail -25
== SessionStart
== SessionStart
== UserPromptSubmit
== PreToolUse  Write src/pricing.js
== PostToolUse Write src/pricing.js
== PreToolUse  Bash node scripts/generate-carriers.js
wrote src/carriers.generated.js
== PostToolUse Bash node scripts/generate-carriers.js
== Stop
== committed 4e3af81
== SessionEnd (folds the log and attributes lines in the background)

Session demo-179  claude-sonnet-4-5  2026-11-11 17:02 → 17:02
  1 prompt(s), 1 response(s), 2 edit(s) (1 via shell), 24 AI line(s) at session end
  4e3af81  local   Add shipment pricing and generated carrier table
      src/carriers.generated.js                      15 line(s)
      src/pricing.js                                  9 line(s)
  24 of 24 AI line(s) committed unchanged or reformatted (100%)
Thinking…

Works end-to-end: 2 edits (1 via shell), 24 AI lines attributed to the commit.

Now verify npm start works, and report --json shows what would be sent. Also verify nothing was sent (no token). Then write README.

Let me run npm start and report --json | head.

cd /sandbox/repo && npm start --silent && echo "=== send --dry-run ===" && npx swarmia-cli send --dry-run && echo "=== report --json (truncated) ===" && npx swarmia-cli report --json | python3 -c "
import json,sys
d=json.load(sys.stdin)
s=d[0] if isinstance(d,list) else d
print(json.dumps(s,indent=2)[:1500])"
Failedswift 42 km → 48.20 EUR
swift    42 km  →  48.20 EUR
bulky   120 km  →  198.00 EUR
night     8 km  →  30.80 EUR
=== send --dry-run ===
0 session(s) would be sent to https://hook.swarmia.com/cli (see `swarmia-cli report --json`)
=== report --json (truncated) ===
Traceback (most recent call last):
  File "<string>", line 4, in <module>
IndexError: list index out of range


Command exited with code 1
Thinking…

report --json returns empty because sessions are only "changed/pending" if pushed? Actually send says 0 would be sent because no token → nothing pending. And report --json shows "exactly what would be sent" → empty since unpushed/no token. Fine — note it in README.

Let me check report --json output raw quickly.

cd /sandbox/repo && npx swarmia-cli report --json | head -c 400; echo; echo "=== git log ==="; git log --oneline; echo "=== status ==="; git status --short
[]
[]

=== git log ===
4e3af81 Add shipment pricing and generated carrier table
0e8c562 Add Swarmia Agent CLI example scaffolding
9ebf5fb Initial workspace
=== status ===
Thinking…

report --json is [] because the commit is not pushed (only pushed commits are sent). Good to document — and to demonstrate it fully one would need a remote. I won't push (no remote / no creds). I'll note it.

Now write README.md for the example. Include:

  • what it is
  • quickstart (npm install, npm run swarmia:install, reload /hooks, npm run swarmia:demo, npm run swarmia:report)
  • how to hook a real agent (Claude Code / Codex trust)
  • sending to Swarmia (token, .swarmia.json, env vars)
  • what's in the repo
  • notes: no token → nothing leaves machine; only pushed commits sent.

Then commit README.

Also need to answer: pricing, and "whether this product will be easy for you to manage".

Let me also double check pricing details from the page: Free up to 9 developers; Standard $45/dev/mo billed annually; Enterprise $55/dev/mo; à la carte: AI adoption & cost $5, Developer surveys $9, Software capitalization $18, Productivity & AI impact $23. Free trial available. Also whether the CLI/AI-impact requires a particular plan: "Coding agent metrics" listed under features; AI adoption & cost plan includes AI adoption/activity/cost metrics. The CLI likely relates to AI activity/coding agent metrics — probably needs the AI adoption & cost plan ($5) or Standard. I shouldn't overclaim. Maybe check help.swarmia.com for the CLI docs to confirm which plan/token. Quick check: search help site for "swarmia-cli" or "agent cli token".

timeout 25 curl -sSL "https://help.swarmia.com/en/?s=cli" -o /tmp/h1.html -w "%{http_code}\n"; timeout 25 curl -sSL "https://help.swarmia.com/" -o /tmp/h2.html -w "%{http_code}\n"; python3 -c "
import re,html
t=open('/tmp/h2.html',encoding='utf-8',errors='ignore').read()
t=re.sub(r'<script.*?</script>','',t,flags=re.S); t=re.sub(r'<style.*?</style>','',t,flags=re.S)
t=re.sub(r'<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'\s+',' ',t)
print(t[:1500])"
404
404
200
 Swarmia documentation | Swarmia docs ⌘ Ctrl k Book a demo Log in More Swarmia documentation Getting started Get started in 15 minutes FEATURES Focus Metrics AI tools Signals PR inbox Working agreements Surveys Software capitalization Swarmia AI Developer overview Notes Forecast Guides Identify and improve delivery bottlenecks Understand how engineering time is spent Benchmarks & comparisons Improve developer experience Understand the impact of AI tools Use Swarmia in meetings and team rituals Drive continuous improvement Prompt library Definitions Developer effort (FTEs) DORA & deployment metrics Throughput Batch size Issue cycle time Flow efficiency Scope creep Definitions FAQ SETTINGS Settings overview Team Organization Integrations Personal Data model Entities Types Resources Security & data retention Pricing & plans Changelog Swarmia support Getting help On this page For the complete documentation index, see llms.txt . This page is also available as Markdown . Copy On this page Swarmia documentation Get help with Swarmia's features, integrations & definitions. Get started in 15 minutes Integrations Configuration and data quality Can't find what you're looking for? Reach out to us at hello@swarmia.com Next Get started in 15 minutes Last updated 6 months ago Was this helpful? Was this helpful? 
timeout 25 curl -sSL "https://www.swarmia.com/pricing/" -o /tmp/p.html; python3 -c "
import re,html
t=open('/tmp/p.html',encoding='utf-8',errors='ignore').read()
t=re.sub(r'<script.*?</script>','',t,flags=re.S); t=re.sub(r'<style.*?</style>','',t,flags=re.S)
t=re.sub(r'<[^>]+>',' ',t); t=html.unescape(t); t=re.sub(r'\s+',' ',t)
i=t.find('Coding agent metrics')
print(t[i-200:i+900])
print('---- free/trial ----')
for m in re.finditer(r'(free|trial|9 developers)', t, re.I):
    print(t[max(0,m.start()-120):m.start()+160].strip()[:280]); print('~')
" | head -40
oling spend in relation to adoption and productivity gains across teams. AI impact metrics Compare how AI-assisted PRs perform against non-assisted ones across
oling spend in relation to adoption and productivity gains across teams. AI impact metrics Compare how AI-assisted PRs perform against non-assisted ones across cycle time, throughput, and batch size. Coding agent metrics Track PR throughput, merge rate, and batch size for agents like Copilot, Cursor, Claude Code, and Codex. Code metrics Analyze PR cycle time, throughput, batch size, and review time across teams and repos. DORA metrics Measure change lead time, deployment frequency, change failure rate, mean time to recovery, and other engineering metrics. Issue metrics Monitor WIP, cycle time, and scope creep from first commit to issue completion. CI visibility Reduce CI wait times and costs with detailed CI/CD insights. Custom reports Create custom reports and saved views to revisit and share with your team. PR inbox Continuously improve your team’s PR metrics based on real-time data. Working agreements Set numeric targets and adopt healthy habits as a team. Sprints Improve sprint planning based on historical data. Developer overview See what each software engineer has been working 
---- free/trial ----
oftware leaders on Engineering Unblocked Benchmarks Find the biggest opportunities for improvement Book: Build Read for free, or buy a physical copy or the Kindle version Read for free → Buy on Amazon → About us Careers Start free trial Get a demo Log in Product Changelog Pricing
~
biggest opportunities for improvement Book: Build Read for free, or buy a physical copy or the Kindle version Read for free → Buy on Amazon → About us Careers Start free trial Get a demo Log in Product Changelog Pricing Customers Learn About us Careers Log in ↗ Pricing & plans B
~
uild Read for free, or buy a physical copy or the Kindle version Read for free → Buy on Amazon → About us Careers Start free trial Get a demo Log in Product Changelog Pricing Customers Learn About us Careers Log in ↗ Pricing & plans Billed monthly Billed annually Best deal Swarmi
~
Read for free, or buy a physical copy or the Kindle version Read for free → Buy on Amazon → About us Careers Start free trial Get a demo Log in Product Changelog Pricing Customers Learn About us Careers Log in ↗ Pricing & plans Billed monthly Billed annually Best deal Swarmia is
~
g Pricing Customers Learn About us Careers Log in ↗ Pricing & plans Billed monthly Billed annually Best deal Swarmia is free for companies with up to 9 developers. Standard $ 45 per developer per month, billed annually Get a demo Drive organization-wide engineering effectiveness
~
t us Careers Log in ↗ Pricing & plans Billed monthly Billed annually Best deal Swarmia is free for companies with up to 9 developers. Standard $ 45 per developer per month, billed annually Get a demo Drive organization-wide engineering effectiveness Includes all features from: AI
~
billed annually Track AI tool adoption and optimize costs AI adoption metrics AI activity metrics AI cost metrics Start free trial Developer surveys $ 9 per developer per month, billed annually Improve developer experience based on feedback from engineers Developer experience sur
~
d annually Track AI tool adoption and optimize costs AI adoption metrics AI activity metrics AI cost metrics Start free trial Developer surveys $ 9 per developer per month, billed annually Improve developer experience based on feedback from engineers Developer experience surveys
~
erience based on feedback from engineers Developer experience surveys Survey reminders on Slack Survey benchmarks Start free trial Software capitalization $ 18 per developer per month, billed annually Track engineering effort and capitalize software development costs Software cap
~
ce based on feedback from engineers Developer experience surveys Survey reminders on Slack Survey benchmarks Start free trial Software capitalization $ 18 per developer per month, billed annually Track engineering effort and capitalize software development costs Software capitali
~
and the impact of AI AI adoption, impact & costs Code, issue & DORA metrics Metrics benchmarks Working agreements Start free trial View the plans Compare the plans Choose up to three plans Standard Enterprise AI adoption & cost Developer surveys Software capitalization Productivi
~
he impact of AI AI adoption, impact & costs Code, issue & DORA metrics Metrics benchmarks Working agreements Start free trial View the plans Compare the plans Choose up to three plans Standard Enterprise AI adoption & cost Developer surveys Software capitalization Productivity &
~
o three plans Standard Enterprise AI adoption & cost Developer surveys Software capitalization Productivity & AI impact Free Standard Enterprise AI adoption & cost Developer surveys Software capitalization Productivity & AI impact Free Standard $45 per dev/mo Enterprise $55 per d
~
I impact Free Standard Enterprise AI adoption & cost Developer surveys Software capitalization Productivity & AI impact Free Standard $45 per dev/mo Enterprise $55 per dev/mo Features AI adoption metrics Track AI tool licenses and spot unused seats or low-adoption teams. AI activ
~
ude Code Codex CodeRabbit GitHub Actions HRIS Proxy agent Frequently asked questions Do I need a credit card to start a free trial? No, you don’t need a credit card to start your free trial. If you’re installing Swarmia from the GitHub Marketplace you might see an Order Summary p
~
ode Codex CodeRabbit GitHub Actions HRIS Proxy agent Frequently asked questions Do I need a credit card to start a free trial? No, you don’t need a credit card to start your free trial. If you’re installing Swarmia from the GitHub Marketplace you might see an Order Summary page a
~
requently asked questions Do I need a credit card to start a free trial? No, you don’t need a credit card to start your free trial. If you’re installing Swarmia from the GitHub Marketplace you might see an Order Summary page asking for your payment information. If this happens, y
~
ntly asked questions Do I need a credit card to start a free trial? No, you don’t need a credit card to start your free trial. If you’re installing Swarmia from the GitHub Marketplace you might see an Order Summary page asking for your payment information. If this happens, you ca
~
dded in Swarmia teams, and you don’t need to pay for those users, for example, product managers. What’s included in the free plan? The free plan contains all the features and functionality in the Standard plan, excluding HRIS integrations and Swarmia AI. The free plan is availabl
~
Thinking…

Good pricing detail. Also FAQ: "What's included in the free plan? all features of Standard, excluding HRIS integrations and Swarmia AI. Available for companies with up to 9 developers." Also monthly billing option exists (billed monthly toggle). Let me grab a couple more FAQ answers briefly (e.g., about who counts as a developer, monthly pricing). Keep it brief though — I have enough.

Now write README.md for the example.

File edits

1 file

Completed
WrittenREADME.mdView changes

/sandbox/repo/README.md

Written content · previous contents were not recorded

# Swarmia Agent CLI — minimal working example
[Swarmia](https://www.swarmia.com) is a hosted engineering-intelligence product
(DORA metrics, PR/issue analytics, AI-adoption and cost reporting). The one part
of it that lives in a repository is the **Swarmia Agent CLI**
([`@swarmia/cli`](https://www.npmjs.com/package/@swarmia/cli)): it hooks into a
coding agent (Claude Code or Codex), records what the agent writes, works out
locally which committed lines those edits became, and — only with a token —
sends line ranges and session metadata to your Swarmia workspace. Never source
code.
This repo is a tiny shipment-pricing app wired up to that CLI, plus a script
that replays one agent session through the real hook contract so you can see
attribution work end to end, with no account and no network calls.
## Quickstart
```sh
npm install # @swarmia/cli as a devDependency (binary included)
npm run swarmia:install # writes 7 hooks into .claude/settings.json
npm run swarmia:config # shows the effective config: no token → nothing is sent
npm run swarmia:demo # replays an agent session, commits, prints the report
npm start # runs the app the "agent" just wrote
```
`npm run swarmia:demo` output:
```
Session demo-179 claude-sonnet-4-5 2026-11-11 17:02 → 17:02
1 prompt(s), 1 response(s), 2 edit(s) (1 via shell), 24 AI line(s) at session end
4e3af81 local Add shipment pricing and generated carrier table
src/carriers.generated.js 15 line(s)
src/pricing.js 9 line(s)
24 of 24 AI line(s) committed unchanged or reformatted (100%)
```
Two things are being demonstrated:
- `src/pricing.js` — written by the agent's `Write` tool, so the edit is in the
hook payload.
- `src/carriers.generated.js` — written by a shell command
(`node scripts/generate-carriers.js`), so there is no edit payload; the CLI
finds those lines by snapshotting the working tree around the command.
- `src/demo.js` is committed by a human and is credited to no session.
## Hooking a real agent
`swarmia-cli install` adds hooks to the agents configured in the repo (a
`.claude/` or `.codex/` directory). Scopes:
| Scope | File | Use it when |
|---|---|---|
| `project` (default) | `.claude/settings.json` (committed) | the whole team is tracked here |
| `local` | `.claude/settings.local.json` (gitignored) | just you, in one repo |
| `user` | `~/.claude/settings.json` | every repo on this machine |
```sh
npx swarmia-cli install --dry-run # preview
npx swarmia-cli install --scope local # just you, here
```
Then activate them: `/hooks` in Claude Code (or a new session). Codex only runs
a hook once *you* trust it — `/hooks` in Codex and approve the six hooks, or
`swarmia-cli install --trust`.
The installed hook command is deliberately inert when the binary is missing
(fresh clone before `npm install`), and hooks always exit 0, so a failure can
never block a tool call.
## Sending sessions to Swarmia
Nothing leaves the machine until a token exists. Get a write-only CLI token
(`swm_cli_…`) from your Swarmia workspace, then either:
```sh
cp .swarmia.json.example .swarmia.json # and paste the token (safe to commit:
# it can only submit sessions)
# or, per machine: ~/.config/swarmia/config.json → { "token": "swm_cli_…" }
# or, per shell: export SWARMIA_TOKEN=swm_cli_…
```
```sh
npx swarmia-cli config # what applies, and from where
npx swarmia-cli send --dry-run # how many sessions would go
npx swarmia-cli send # send now (normally automatic at session end)
```
Useful knobs: `SWARMIA_ENDPOINT` (default `https://hook.swarmia.com/cli`),
`SWARMIA_PROMPTS=0` to withhold your prompts and the agent's replies.
Note: **only pushed commits are ever attributed for delivery.** This example has
no remote, so `report --json` prints `[]` and `send --dry-run` says `0 session(s)`
— that is correct behaviour, not a failure. Add a remote and push, and the
session becomes sendable.
## What each command does
| Command | What it does |
|---|---|
| `install` | add hooks to the coding agents in a repo (`--trust` for Codex) |
| `report` | recent sessions and the committed lines they produced |
| `config` | effective configuration and where it comes from |
| `send` | send changed sessions to Swarmia now |
| `process` | fold the event log and attribute sessions (runs automatically) |
| `state-dir` | where this repo's local state lives (outside the repo) |
| `log-hook` | the hook itself; agents call it |
## Files
```
.claude/settings.json hooks installed by swarmia-cli (project scope)
.swarmia.json.example template for the write-only CLI token
package.json npm scripts wrapping the CLI
scripts/demo-session.sh replays one session through the hook contract
scripts/generate-carriers.js codegen run through a Bash tool call
src/demo.js human-written entry point
src/pricing.js written by the demo "agent" (Write tool)
src/carriers.generated.js written by the demo "agent" (shell command)
```
Local state (`events.jsonl`, folded sessions, snapshots) lives in
`~/.local/state/swarmia/repos/<hash>/`, outside the repository — print it with
`npx swarmia-cli state-dir`.
## Layout of the demo script
`scripts/demo-session.sh` emits exactly the payloads Claude Code puts on a hook's
stdin, in the order a real session would:
1. `SessionStart`, `UserPromptSubmit`
2. `PreToolUse`/`PostToolUse` for a `Write` of `src/pricing.js`
3. `PreToolUse`/`PostToolUse` for a `Bash` codegen command (the pair needs a
shared `tool_use_id`, which is how the before-snapshot is matched to the
after-diff)
4. `Stop` — the agent's final reply goes in `last_assistant_message`
5. the developer commits
6. `SessionEnd` — folds the log and attributes lines in a detached background
process; the script then runs `process` synchronously and prints `report`
Run it as often as you like: each run is a new session and a new commit.
Thinking…

Verify README claims: the demo re-run works (each run new session/commit). Let me re-run once to confirm idempotency and that report shows two sessions. Then commit README + .gitignore adjustments? node_modules already ignored. Also .claude/settings.local.json not present.

Run demo again and check report --limit 5 shows both sessions.

cd /sandbox/repo && timeout 120 npm run swarmia:demo --silent 2>&1 | tail -14 && echo "=== git log ===" && git log --oneline | head -4
== SessionStart
== SessionStart
== UserPromptSubmit
== PreToolUse  Write src/pricing.js
== PostToolUse Write src/pricing.js
== PreToolUse  Bash node scripts/generate-carriers.js
wrote src/carriers.generated.js
== PostToolUse Bash node scripts/generate-carriers.js
== Stop
On branch main
Untracked files:
  (use "git add <file>..." to include in what will be committed)
	README.md

nothing added to commit but untracked files present (use "git add" to track)
=== git log ===
4e3af81 Add shipment pricing and generated carrier table
0e8c562 Add Swarmia Agent CLI example scaffolding
9ebf5fb Initial workspace
Thinking…

Second run fails at commit because files unchanged (already committed, identical content). Need to make the demo re-runnable: either make the generated content vary (e.g. include a timestamp/rate tweak), or make the script tolerant. Better: have generate-carriers.js vary slightly per run? That's artificial. Simpler and honest: the demo script commits only if there's something to commit; and to make repeats meaningful, add a small change each run? Hmm.

Cleanest: make the demo script git commit --allow-empty? No — attribution needs lines.

Option: on re-run, the Write content is the same → no new lines. So make the script amend? Better approach: the demo script accepts an optional "variant" — e.g. it appends a new carrier rate line or a comment with the run number, making each run produce genuinely new agent-authored lines. That's realistic (agent iterating).

Let me add: the pricing module gets a --surcharge iteration? Keep simple: the CONTENT includes a run marker comment line and a small feature that changes per run? Simplest: script takes optional arg RUN and writes an extra line into pricing.js like // iteration N — meh.

Alternative: make the demo reset its own artifacts first: if src/pricing.js exists, remove both files and commit their removal as a human commit? That pollutes history.

I think the cleanest, most realistic re-runnable demo: each run the agent adds one new carrier to the codegen table (a genuinely new line), and adds a new pricing helper. Let me implement: generate-carriers.js reads existing table and appends a new carrier from a list (deterministic by count). And pricing.js write includes a new function? Simpler: only the generated file grows per run — that still demonstrates both paths on run 1, and on later runs shows shell-authored new lines.

Hmm, but the Write path would produce no new lines on repeat runs, so report would show only generated file. Acceptable, but let me make it nicer: pricing.js content varies by adding a line for each carrier added? Complexity.

Alternative pragmatic approach: the demo script detects a repeat run and reverts the previous demo commit's files (git rm + human commit "Revert demo output"), so each run starts clean. That's actually realistic-ish and keeps the Write path fresh. But adds a human commit each time.

Simplest defensible: make each run produce a fresh, differently-named pair of files? e.g. src/pricing.js is stable but demo writes src/pricing.js plus a per-run src/quote-<n>.js? Ugly.

Let me reconsider: how important is re-runnability? I claimed it in README. I could instead adjust README: "Run it once per fresh checkout; to run it again, reset the demo commit first (git reset --hard HEAD~1)" — honest and simple. But nicer UX: script handles it itself: if the previous demo commit is HEAD and its message matches, undo it (reset --hard HEAD~1) before starting, so each run replays cleanly. That keeps history to one demo commit. Risk: destructive reset. Only when HEAD is exactly the demo commit — acceptable and documented, with a guard that the tree is clean.

I'll implement that: at start, if git log -1 --format=%s == demo message and tree clean → git reset --hard HEAD~1 and rm -f the two files (reset --hard restores removal of committed files? reset --hard to parent commit removes files added in the commit — yes, files added in that commit get deleted). Good.

Add a --keep flag? Not needed. Document in README.

Let me implement.

File edits

1 file

Completed
Modifiedscripts/demo-session.sh+10−2View changes

/sandbox/repo/scripts/demo-session.sh

Replacement excerpt

+MSG="Add shipment pricing and generated carrier table"
+
+# Re-runnable: if the previous run's commit is still HEAD and the tree is clean,
+# undo it so this run replays from the same starting point.
+if [ "$(git log -1 --format=%s)" = "$MSG" ] && [ -z "$(git status --porcelain)" ]; then
+ echo "== undoing the previous demo run ($(git rev-parse --short HEAD))"
+ git reset --hard -q HEAD~1
+fi
+
SESSION="demo-$(date +%s)"

Replacement excerpt

-git -c user.name="Demo Dev" -c user.email="dev@example.com" \
- commit -q -m "Add shipment pricing and generated carrier table"
+git -c user.name="Demo Dev" -c user.email="dev@example.com" commit -q -m "$MSG"
Thinking…

Note: README.md is currently untracked → git status --porcelain non-empty → guard won't trigger on first re-run until README committed. Commit README first, then test twice.

Wait: the demo's commit step does git add src/pricing.js src/carriers.generated.js only — good, README won't be swept in.

Commit README + script changes, then run demo twice.

cd /sandbox/repo && git add README.md scripts/demo-session.sh && git commit -q -m "Document the Swarmia Agent CLI example" && timeout 120 npm run swarmia:demo --silent 2>&1 | tail -12 && echo "===== second run =====" && timeout 120 npm run swarmia:demo --silent 2>&1 | tail -12 && git log --oneline | head -3 && git status --short
== SessionStart
== SessionStart
== UserPromptSubmit
== PreToolUse  Write src/pricing.js
== PostToolUse Write src/pricing.js
== PreToolUse  Bash node scripts/generate-carriers.js
wrote src/carriers.generated.js
== PostToolUse Bash node scripts/generate-carriers.js
== Stop
On branch main
nothing to commit, working tree clean
===== second run =====
== SessionStart
== UserPromptSubmit
== PreToolUse  Write src/pricing.js
== PostToolUse Write src/pricing.js
== PreToolUse  Bash node scripts/generate-carriers.js
wrote src/carriers.generated.js
== PostToolUse Bash node scripts/generate-carriers.js
== Stop
On branch main
nothing to commit, working tree clean
b3dda5a Document the Swarmia Agent CLI example
4e3af81 Add shipment pricing and generated carrier table
0e8c562 Add Swarmia Agent CLI example scaffolding
Thinking…

The undo guard didn't trigger because HEAD is now "Document the..." commit, not the demo commit. The demo files are already committed from the earlier run (4e3af81), so nothing to commit.

Better approach: make the guard reset the demo artifacts whenever the files already exist and match — i.e., at the start, if src/pricing.js exists, remove the two demo output files (they're committed; removing them makes the tree dirty, and the demo recreates them). But then the commit would show no diff (files identical to HEAD) → still nothing to commit.

Cleanest robust approach: the demo run should produce a fresh diff each time. Options: (a) Find the demo commit in history and reset to before it — destructive if there are commits after. (b) Delete the files AND commit the deletion as part of setup ("Remove previous demo output") — then the demo re-adds them → real diff each time. History grows by 2 commits per run but it's honest and always works.

Actually simpler (c): make the agent's output differ per run in a natural way — the codegen adds a new carrier each run (a genuinely new line), and pricing.js gains a line too. Hmm, unbounded growth.

I'll go with (b) but keep it tidy: setup step removes previous outputs and commits as a human commit only if they exist. Let me instead do (a') : reset --soft?

Let me think about what's clearest for a reader: "The demo is re-runnable: if the files it writes are already committed, it removes them first (one human commit), then the agent writes them again." That's understandable and safe (no history rewriting).

Implement: after the guard block, replace with:

# Re-runnable: if a previous run's output is already committed, remove it first
# (as the developer would), so the agent's writes are a real diff again.
if git ls-files --error-unmatch src/pricing.js >/dev/null 2>&1; then
  echo "== removing the previous run's output"
  git rm -q src/pricing.js src/carriers.generated.js
  git -c ... commit -q -m "Remove previous demo output"
fi

That works regardless of history. Let me replace the undo guard with this.

Also note: git rm removes files from worktree; the demo recreates them.

Test twice.

File edits

1 file

Completed
Modifiedscripts/demo-session.sh+8−6View changes

/sandbox/repo/scripts/demo-session.sh

Replacement excerpt

MSG="Add shipment pricing and generated carrier table"
+GIT=(git -c user.name="Demo Dev" -c user.email="dev@example.com")
-# Re-runnable: if the previous run's commit is still HEAD and the tree is clean,
-# undo it so this run replays from the same starting point.
-if [ "$(git log -1 --format=%s)" = "$MSG" ] && [ -z "$(git status --porcelain)" ]; then
- echo "== undoing the previous demo run ($(git rev-parse --short HEAD))"
- git reset --hard -q HEAD~1
+# Re-runnable: if a previous run's output is already committed, the "developer"
+# removes it first, so what the agent writes is a real diff again.
+if git ls-files --error-unmatch src/pricing.js >/dev/null 2>&1; then
+ echo "== removing the previous run's output"
+ git rm -q src/pricing.js src/carriers.generated.js
+ "${GIT[@]}" commit -q -m "Remove previous demo output"
fi

Replacement excerpt

-git -c user.name="Demo Dev" -c user.email="dev@example.com" commit -q -m "$MSG"
+"${GIT[@]}" commit -q -m "$MSG"
Thinking…

Careful: GIT=(git -c ...) array then git rm uses plain git — fine.

Test twice now.

cd /sandbox/repo && timeout 120 npm run swarmia:demo --silent 2>&1 | tail -12 && echo "===== run 2 =====" && timeout 120 npm run swarmia:demo --silent 2>&1 | tail -12 && echo "=== log ===" && git log --oneline | head -6 && git status --short
Session demo-179 claude-sonnet-4-5 2026-11-11 17:03 → 17:03

Session demo-179  claude-sonnet-4-5  2026-11-11 17:03 → 17:03
  1 prompt(s), 1 response(s), 1 edit(s), 9 AI line(s) at session end
  40487db  local   Add shipment pricing and generated carrier table
      src/pricing.js                                  9 line(s)
  9 of 9 AI line(s) committed unchanged or reformatted (100%)

Session demo-179  claude-sonnet-4-5  2026-11-11 17:02 → 17:02
  1 prompt(s), 1 response(s), 1 edit(s), 9 AI line(s) at session end
  40487db  local   Add shipment pricing and generated carrier table
      src/pricing.js                                  9 line(s)
  9 of 9 AI line(s) committed unchanged or reformatted (100%)
===== run 2 =====
  cc277b8  local   Add shipment pricing and generated carrier table
      src/carriers.generated.js                      15 line(s)
      src/pricing.js                                  9 line(s)
  24 of 24 AI line(s) committed unchanged or reformatted (100%)

Session demo-179  claude-sonnet-4-5  2026-11-11 17:03 → 17:03
  1 prompt(s), 1 response(s), 1 edit(s), 9 AI line(s) at session end
  40487db  local   Add shipment pricing and generated carrier table
      src/pricing.js                                  9 line(s)
  cc277b8  local   Add shipment pricing and generated carrier table
      src/pricing.js                                  9 line(s)
  9 of 9 AI line(s) committed unchanged or reformatted (100%)
=== log ===
cc277b8 Add shipment pricing and generated carrier table
02ca8c8 Remove previous demo output
40487db Add shipment pricing and generated carrier table
4893f3a Remove previous demo output
b3dda5a Document the Swarmia Agent CLI example
4e3af81 Add shipment pricing and generated carrier table
 M scripts/demo-session.sh
Thinking…

Interesting: run at 17:03 only attributed 9 lines (carriers.generated.js missing) because the leftover file from before was... hmm — in that run, git rm removed both files; then generate-carriers.js recreated it. But only pricing.js was attributed. Why?

Ah: the earlier failed runs (before the fix) left events? No. Let's think: in run 1 of this pair, before it there were prior runs where carriers.generated.js existed and was committed. git rm removed it → dirty (deleted). PreToolUse snapshot records it as deleted; PostToolUse diff: file recreated with identical content to HEAD → "lines restored from git"/not authored → excluded! That's the AuthoredOnly rule: a line whose normalized text occurs in HEAD isn't credited. Indeed: after git rm + commit "Remove previous demo output", HEAD no longer has the file... wait, it was committed as removed, so HEAD lacks it. Hmm.

But in run 1 of this pair: order was git rm + commit, then the demo wrote files. So HEAD lacks carriers.generated.js. Then why not authored? Maybe the "last snapshot" cache (mtime/size reuse) or the pending snapshot keyed by BASH_ID — that worked in the earlier successful run.

Wait — maybe the difference: in the first run of this pair, the guard ran and git rm happened before SessionStart... irrelevant.

Hmm, note run 2 correctly got 24 lines. Run 1 (17:03) got 9. Difference: in run 1, the working tree had leftover untracked?? Actually before run 1, src/carriers.generated.js existed (committed). git rm deleted it. Then codegen wrote it back with the same size and mtime? mtime would be new. The snapshot "last" cache compares size and mtime — new mtime → re-read. Hmm.

Alternatively the PreToolUse snapshot for the Bash command: dirtyPaths from git status. Before the command, the worktree had src/pricing.js (untracked, just written). Then the command created carriers.generated.js. Diff should include it.

Unless... the deletion left a stale last entry marking the path as Deleted with no blob, and Changes skips files the command deleted — no, it was recreated.

Wait, maybe the issue: run 1's PostToolUse Bash snapshot found changes but attribution classed lines as not authored because the file content existed in the stash/HEAD of commit 40487db? No, HEAD at that time was 4893f3a (removal). Hmm, but attribution compares against HEAD at processing time — HEAD is cc277b8?? No, run 1's commit is 40487db.

Hold on — attribution uses "sources" only for stopped git operations. The authored rule: "a line's normalized text must occur more often afterwards than in the file before the command or in HEAD". At snapshot time HEAD = 4893f3a which does NOT contain the file. So lines should count.

Unless run 1 executed while HEAD still contained the file... The git rm + commit happened at the beginning, so no.

Hmm, but there's another subtlety: the record for the Bash PostToolUse — did it have changes? The 17:03 session shows "1 edit(s)" meaning no shell change record at all. So the PostToolUse didn't produce changes. Possibly store.Load(BASH_ID) failed → returned early. Why? Because PreToolUse saved pending only if len(pre) > 0... pre would contain the repo snapshot (pricing.js dirty) → non-empty. Unless PreToolUse marked it read-only? "node scripts/generate-carriers.js" — not read-only.

OR: the PreToolUse event ran BEFORE the git rm commit in run 1? No.

Wait, actually — look at run 1 output: it printed only the report tail (12 lines) — I cut the earlier output. Maybe the "== removing the previous run's output" happened, fine.

Alternative explanation: in run 1, src/pricing.js was written identically to what was in HEAD before the removal... no, it was removed too.

Hmm, let me consider the snapshot budget: git status in /sandbox/repo with node_modules — node_modules is gitignored, so status is fast. Fine.

Maybe the pending snapshot was pruned: store.Prune(time.Now()) on PreToolUse removes stale pendings. Fine.

Let me just re-run and inspect the session JSON for the shell change record + look at whether snapshot marker says something. Actually let me reproduce with fresh state and check. Simpler: run the demo a third time and inspect the newest session JSON for 'snapshot' markers. But sessions record after folding may include snapshot info? The session JSON earlier didn't show snapshot markers. Let's instead capture events.jsonl before processing: run demo with the final process disabled? The SessionEnd hook triggers a detached process; events.jsonl gets consumed. I can copy events.jsonl right after the Bash PostToolUse by pausing — hard.

Alternative: replicate the Bash snapshot manually in a scratch state to debug. Use SWARMIA_LOG_FILE to a temp path and run only the Bash pre/post pair, then inspect the record's changes/snapshot fields, without SessionEnd (no processing).

Let me do that experiment: fresh temp log file, emit PreToolUse Bash (with tool_use_id), run the codegen after removing the file, emit PostToolUse, then cat the log.

Scenario A: file exists & committed (HEAD has it), remove via plain rm (dirty delete), then regenerate → check if changes recorded. That mirrors run 1 except the removal was committed. Let me test both.

cd /sandbox/repo && CLI=node_modules/@swarmia/cli/bin/swarmia-cli && export SWARMIA_LOG_FILE=/tmp/exp1.jsonl && rm -f /tmp/exp1.jsonl && rm -rf "$(npx swarmia-cli state-dir)/snapshots" 
S=exp1; ID=t1; CWD=$PWD; CMD="node scripts/generate-carriers.js"
# HEAD contains carriers.generated.js; delete it in the worktree (uncommitted), then regenerate
rm -f src/carriers.generated.js
jq -nc --arg s $S --arg cwd "$CWD" --arg id $ID --arg c "$CMD" '{hook_event_name:"PreToolUse",session_id:$s,cwd:$cwd,tool_name:"Bash",tool_use_id:$id,tool_input:{command:$c}}' | $CLI log-hook
$CMD >/dev/null
jq -nc --arg s $S --arg cwd "$CWD" --arg id $ID --arg c "$CMD" '{hook_event_name:"PostToolUse",session_id:$s,cwd:$cwd,tool_name:"Bash",tool_use_id:$id,tool_input:{command:$c},tool_response:{stdout:"ok"}}' | $CLI log-hook
python3 -c "
import json
for l in open('/tmp/exp1.jsonl'):
    r=json.loads(l); e=r['event']
    print(e['hook_event_name'], 'snapshot=',r.get('snapshot'), 'snapshotted=',r.get('snapshotted'), 'changes=',json.dumps(r.get('changes'))[:200])
"; git checkout -- src/carriers.generated.js 2>/dev/null; git status --short
PreToolUse snapshot= None snapshotted= ['/sandbox/repo'] changes= null
PreToolUse snapshot= None snapshotted= ['/sandbox/repo'] changes= null
PostToolUse snapshot= None snapshotted= ['/sandbox/repo'] changes= null
 M scripts/demo-session.sh
Thinking…

Confirmed: when the command restores a file whose content is identical to HEAD, the lines are not credited ("restored from git" / not authored). That's the documented conservative rule — correct behaviour.

So run 1's 9-line result happened because... in run 1, at the time of the Bash command, HEAD (4893f3a "Remove previous demo output") — hmm, did that commit actually remove carriers.generated.js? Yes, git rm both. So HEAD lacked it. Then why no changes?

Wait — maybe in run 1 the previous state had src/carriers.generated.js present but not tracked? Let me check history: at 17:02 run, files committed in cc277b8? No — 17:02 run was the earlier successful one producing 4e3af81. Then two failed runs (nothing to commit) which still wrote the files (identical, so tree clean). Then run at 17:03: git rm → commit 4893f3a removes both. Then demo writes pricing.js (9 lines attributed ✓) and codegen writes carriers.generated.js → should be authored since HEAD lacks it...

Unless the pending snapshot's "last" blob cache: PreToolUse recorded the file as Deleted (entry with no blob). PostToolUse diff: file exists now with content X. Compare against before-state (deleted) → all lines added. AuthoredOnly then excludes lines occurring in HEAD... HEAD=4893f3a lacks the file → should be authored. Hmm, but AuthoredOnly may also consider the pre-snapshot Head field (st.Head at pre time) — same commit.

Hmm, maybe attribution compares against the commit it's matching (40487db) — no, that's the commit containing the lines.

Alternatively: maybe in run 1 the git rm commit happened after something? Let me look at the actual 17:03 session record more concretely — check the session JSON for that session (demo-179...17:03) to see the Bash tool entry and whether an edit was recorded.

Let me look at all sessions in state dir.

SD=$(cd /sandbox/repo && npx swarmia-cli state-dir); ls $SD/sessions; for f in $SD/sessions/*.json; do python3 -c "
import json,sys
d=json.load(open('$f'))
print('---', d['id'], d['started_at'])
print('tools:', [(t['name'], t.get('command') or t.get('file')) for t in d.get('tools',[])])
print('edits:', [(e['path'], sum(len(h) for h in e['hunks'])) for e in d.get('edits',[])])
print('ai_lines:', d.get('ai_lines_by_root'))
"; done
demo-1794416551.json
demo-1794416551.json
demo-1794416578.json
demo-1794416590.json
demo-1794416606.json
demo-1794416607.json
--- demo-1794416551 2026-11-11T17:02:31Z
tools: [('Write', '/sandbox/repo/src/pricing.js'), ('Bash', 'node scripts/generate-carriers.js')]
edits: [('src/pricing.js', 11), ('src/carriers.generated.js', 15)]
ai_lines: {'/sandbox/repo': {'src/carriers.generated.js': 15, 'src/pricing.js': 9}}
--- demo-1794416578 2026-11-11T17:02:58Z
tools: [('Write', '/sandbox/repo/src/pricing.js'), ('Bash', 'node scripts/generate-carriers.js')]
edits: [('src/pricing.js', 11)]
ai_lines: {'/sandbox/repo': {'src/pricing.js': 9}}
--- demo-1794416590 2026-11-11T17:03:10Z
tools: [('Write', '/sandbox/repo/src/pricing.js'), ('Bash', 'node scripts/generate-carriers.js')]
edits: [('src/pricing.js', 11)]
ai_lines: {'/sandbox/repo': {'src/pricing.js': 9}}
--- demo-1794416606 2026-11-11T17:03:26Z
tools: [('Write', '/sandbox/repo/src/pricing.js'), ('Bash', 'node scripts/generate-carriers.js')]
edits: [('src/pricing.js', 11), ('src/carriers.generated.js', 15)]
ai_lines: {'/sandbox/repo': {'src/carriers.generated.js': 15, 'src/pricing.js': 9}}
--- demo-1794416607 2026-11-11T17:03:27Z
tools: [('Write', '/sandbox/repo/src/pricing.js'), ('Bash', 'node scripts/generate-carriers.js')]
edits: [('src/pricing.js', 11), ('src/carriers.generated.js', 15)]
ai_lines: {'/sandbox/repo': {'src/carriers.generated.js': 15, 'src/pricing.js': 9}}
Thinking…

Sessions 17:02:58 and 17:03:10 lacked the shell edit — those were the two "nothing to commit" runs where the file already existed unchanged in HEAD (so restoring/rewriting identical content = not authored). That matches the rule. And 17:03:26/27 (with git rm) both got 15 lines. So my earlier reading of "run 1 = 9 lines" was actually the older session displayed by report --limit 3; the newest run did attribute 24 lines. Everything is consistent and correct.

Great — the demo is deterministic now. Let me clean up: reset history? The history now has repeated demo commits (4893f3a, 40487db, 02ca8c8, cc277b8). For a clean example, I'd like history: Initial workspace → scaffolding → docs → one demo commit. Let me squash/reset: git reset --soft b3dda5a then re-run demo once → gives clean two-commit example. Also clear stale sessions from state dir so report shows just one session.

Steps:

  1. git reset --hard b3dda5a (docs commit) — removes demo commits and files.
  2. clear state dir (sessions, snapshots, events).
  3. run demo once → clean single demo commit + single session in report.
  4. commit the modified scripts/demo-session.sh (currently modified — need to commit that first, before reset!). Order: commit script change first (amend into docs commit or new commit), then reset isn't needed... Actually demo commits come after b3dda5a; the script change is uncommitted. Let me:
    • git stash? Simpler: git add scripts/demo-session.sh && git commit -m "Make the demo re-runnable", then git reset --soft to that commit? The demo commits are interleaved: b3dda5a(docs) → 4893f3a → 40487db → 02ca8c8 → cc277b8 (HEAD). The script change is on top of cc277b8 uncommitted.
    • Plan: save the script content, git reset --hard b3dda5a, restore script, commit it, clear state, run demo once.

Let's do it.

cd /sandbox/repo && cp scripts/demo-session.sh /tmp/demo-session.sh && git reset --hard -q b3dda5a && cp /tmp/demo-session.sh scripts/demo-session.sh && chmod +x scripts/demo-session.sh && git add scripts/demo-session.sh && git commit -q -m "Make the demo session re-runnable" && SD=$(npx swarmia-cli state-dir) && rm -rf "$SD"/sessions "$SD"/snapshots "$SD"/events.jsonl && git log --oneline && git status --short
82e1fc1 Make the demo session re-runnable
82e1fc1 Make the demo session re-runnable
b3dda5a Document the Swarmia Agent CLI example
4e3af81 Add shipment pricing and generated carrier table
0e8c562 Add Swarmia Agent CLI example scaffolding
9ebf5fb Initial workspace
Thinking…

Wait: 4e3af81 (the first demo commit) is still in history below b3dda5a. And the files src/pricing.js & carriers.generated.js exist from it. That's fine — the demo will remove & re-add them on next run. But for a clean example repo, maybe I want history without demo output at all, so a user can run the demo themselves and see it happen.

Let me reset to 0e8c562 (scaffolding) + re-apply docs & re-runnable commits... simpler: soft-reset to 0e8c562 keeping worktree, then remove src/pricing.js and src/carriers.generated.js, then commit "docs" as one commit. Then history: Initial → scaffolding → docs. Clean.

Then run the demo once? I'd rather leave the repo without demo output so the user runs it. But I want to verify the final state works. I'll run it, verify, then reset --hard back to the docs commit and clear state, leaving a pristine example. Hmm, but then the report will be empty for the user — that's fine and expected; README shows expected output.

Actually leaving one demo commit is also fine and shows a real result. I prefer pristine: user runs npm run swarmia:demo.

Let's do: reset --soft 0e8c562; rm the two demo files; commit all as "Document the example and make the demo re-runnable"; verify tree; then run demo to verify; then reset --hard to that commit and clear state dir.

cd /sandbox/repo && git reset --soft -q 0e8c562 && rm -f src/pricing.js src/carriers.generated.js && git add -A && git status --short && git commit -q -m "Document the example and make the demo re-runnable" && git log --oneline
A README.md
A  README.md
M  scripts/demo-session.sh
607495a Document the example and make the demo re-runnable
0e8c562 Add Swarmia Agent CLI example scaffolding
9ebf5fb Initial workspace
Thinking…

Now verify from pristine state: run demo twice (to prove re-runnable), then reset back.

Note: the demo's "remove previous run's output" branch won't trigger on first run now (files not tracked). Good.

Run 1 → creates files + commit. Run 2 → removes + recreates. Verify both attribute 24 lines. Then reset --hard 607495a and clear state.

Also README mentions "each run is a new session and a new commit" — now with the removal commit too. Let me update README wording slightly to describe the removal step. Also README's file list includes src/pricing.js and src/carriers.generated.js which don't exist until the demo runs — adjust to "(created by the demo)".

Also README shows report output with commit 4e3af81 — fine as sample.

Let me run the verification first.

cd /sandbox/repo && timeout 120 npm run swarmia:demo --silent 2>&1 | tail -9 && echo "===== run 2 =====" && timeout 120 npm run swarmia:demo --silent 2>&1 | tail -9 && echo "=== report ===" && npx swarmia-cli report --limit 5 2>&1 | head -20
== committed e7ef781
== committed e7ef781
== SessionEnd (folds the log and attributes lines in the background)

Session demo-179  claude-sonnet-4-5  2026-11-11 17:04 → 17:04
  1 prompt(s), 1 response(s), 2 edit(s) (1 via shell), 24 AI line(s) at session end
  e7ef781  local   Add shipment pricing and generated carrier table
      src/carriers.generated.js                      15 line(s)
      src/pricing.js                                  9 line(s)
  24 of 24 AI line(s) committed unchanged or reformatted (100%)
===== run 2 =====
Session demo-179  claude-sonnet-4-5  2026-11-11 17:04 → 17:04
  1 prompt(s), 1 response(s), 2 edit(s) (1 via shell), 24 AI line(s) at session end
  e7ef781  local   Add shipment pricing and generated carrier table
      src/carriers.generated.js                      15 line(s)
      src/pricing.js                                  9 line(s)
  4938725  local   Add shipment pricing and generated carrier table
      src/carriers.generated.js                      15 line(s)
      src/pricing.js                                  9 line(s)
  24 of 24 AI line(s) committed unchanged or reformatted (100%)
=== report ===
Session demo-179  claude-sonnet-4-5  2026-11-11 17:04 → 17:04
  1 prompt(s), 1 response(s), 2 edit(s) (1 via shell), 24 AI line(s) at session end
  e7ef781  local   Add shipment pricing and generated carrier table
      src/carriers.generated.js                      15 line(s)
      src/pricing.js                                  9 line(s)
  4938725  local   Add shipment pricing and generated carrier table
      src/carriers.generated.js                      15 line(s)
      src/pricing.js                                  9 line(s)
  24 of 24 AI line(s) committed unchanged or reformatted (100%)

Session demo-179  claude-sonnet-4-5  2026-11-11 17:04 → 17:04
  1 prompt(s), 1 response(s), 2 edit(s) (1 via shell), 24 AI line(s) at session end
  e7ef781  local   Add shipment pricing and generated carrier table
      src/carriers.generated.js                      15 line(s)
      src/pricing.js                                  9 line(s)
  4938725  local   Add shipment pricing and generated carrier table
      src/carriers.generated.js                      15 line(s)
      src/pricing.js                                  9 line(s)
  24 of 24 AI line(s) committed unchanged or reformatted (100%)
Thinking…

Both runs attribute 24 lines. Re-runnable works.

Now reset to pristine (607495a) and clear state, then update README wording about re-runs (removal commit) and file list.

cd /sandbox/repo && git reset --hard -q 607495a && SD=$(npx swarmia-cli state-dir) && rm -rf "$SD"/sessions "$SD"/snapshots "$SD"/events.jsonl && git log --oneline && git status --short && ls src scripts .claude
607495a Document the example and make the demo re-runnable
607495a Document the example and make the demo re-runnable
0e8c562 Add Swarmia Agent CLI example scaffolding
9ebf5fb Initial workspace
.claude:
settings.json

scripts:
demo-session.sh
generate-carriers.js

src:
demo.js
Thinking…

Now update README: file list marks the two generated files as created by the demo; re-run note explains the removal commit; also update sample output commit hash maybe (keep as-is but use e7ef781? fine, it's illustrative — let me use the fresh one for accuracy). Also mention npm start requires running the demo first.

Edits to README:

  1. Quickstart note: npm start after demo.
  2. Sample output block: update hash to e7ef781 (already matches what I'd show).
  3. Files section: annotate.
  4. Last section: re-run behaviour.

File edits

1 file

Completed
ModifiedREADME.md+11−5View changes

/sandbox/repo/README.md

Replacement excerpt

npm run swarmia:demo # replays an agent session, commits, prints the report
npm start # runs the app the "agent" just wrote
```
`npm run swarmia:demo` output:
```
-Session demo-179 claude-sonnet-4-5 2026-11-11 17:02 → 17:02
+Session demo-179 claude-sonnet-4-5 2026-11-11 17:04 → 17:04
1 prompt(s), 1 response(s), 2 edit(s) (1 via shell), 24 AI line(s) at session end
- 4e3af81 local Add shipment pricing and generated carrier table
+ e7ef781 local Add shipment pricing and generated carrier table
src/carriers.generated.js 15 line(s)
src/pricing.js 9 line(s)
24 of 24 AI line(s) committed unchanged or reformatted (100%)
```

Replacement excerpt

scripts/demo-session.sh replays one session through the hook contract
scripts/generate-carriers.js codegen run through a Bash tool call
src/demo.js human-written entry point
-src/pricing.js written by the demo "agent" (Write tool)
-src/carriers.generated.js written by the demo "agent" (shell command)
+src/pricing.js written by the demo "agent" (Write tool) ← created by the demo
+src/carriers.generated.js written by the demo "agent" (shell command) ← created by the demo
```

Replacement excerpt

-Run it as often as you like: each run is a new session and a new commit.
+Run it as often as you like: each run is a new session. If the previous run's
+output is still committed, the script removes it first (as one human commit) so
+the agent's writes are a real diff again — otherwise there would be nothing new
+to attribute. Note the flip side of that rule: a shell command that restores
+content already in `HEAD` is credited with nothing, because the CLI can't tell it
+apart from `git checkout`. Lines it can't attribute are reported as unknown,
+never as human.
Thinking…

Also add a "Pricing" note in README? The user asked me to tell them pricing — I'll do that in my reply, but a short section in README is useful. Add a brief "What Swarmia costs" section near the top or end. Let me append a section before "## Files" or at the end.

Also verify final state one more time (run demo once, then reset). Actually I already verified; the README changed only. Let me do a final quick sanity check: fresh clone-ish test? Skip.

Add pricing section, commit.

File edits

1 file

Completed
ModifiedREADME.md+18−0View changes

/sandbox/repo/README.md

Replacement excerpt

+## What Swarmia costs
+
+From [swarmia.com/pricing](https://www.swarmia.com/pricing/) (per developer per
+month, billed annually; free trial, no credit card):
+
+- **Free** — companies with up to 9 developers; everything in Standard except
+ HRIS integrations and Swarmia AI.
+- **Standard $45** — all four feature sets below.
+- **Enterprise $55** — adds on-premise integrations, HR system integrations,
+ professional services.
+- À la carte, pick up to three: **AI adoption & cost $5**, **Developer surveys
+ $9**, **Software capitalization $18**, **Productivity & AI impact $23**.
+
+Only developers added to Swarmia teams are billed. The Agent CLI feeds the AI
+activity / coding-agent metrics, so the $5 AI adoption & cost plan is the cheap
+way to light up what this example collects; the CLI itself is open source and
+free to run locally, as here.
+
## What each command does
Thinking…

Verify the "Free plan" and "only developers added to teams are billed" claims — I saw FAQ snippets: "What's included in the free plan? The free plan contains all the features and functionality in the Standard plan, excluding HRIS integrations and Swarmia AI. The free plan is availabl..." and "...ntly added in Swarmia teams, and you don't need to pay for those users, for example, product managers." Good.

Also "CLI itself is open source" — repo has LICENSE. Let me check license type quickly, and also whether help docs mention the CLI token page (to give the user a pointer). Then commit.

timeout 20 curl -sS https://raw.githubusercontent.com/swarmia/swarmia-cli/main/LICENSE | head -3; echo "=== llms.txt search ==="; timeout 25 curl -sSL https://help.swarmia.com/llms.txt 2>/dev/null | grep -i -E "cli|agent|ai tool" | head -20
Copyright (c) 2026 Swarmia Oy
Copyright (c) 2026 Swarmia Oy

Permission is hereby granted, free of charge, to any person obtaining a copy
=== llms.txt search ===
- [Get started in 15 minutes](https://help.swarmia.com/getting-started/get-started-in-15-minutes.md): Connect your code hosting platform, issue tracker, AI tools, messaging app, and create Swarmia teams to get started.
- [AI tools](https://help.swarmia.com/features/ai-tools.md): Track AI coding tool adoption, activity, impact, and agent usage in Swarmia.
- [AI readiness](https://help.swarmia.com/features/ai-tools/ai-readiness.md): See whether your repositories give AI agents the context and delivery pipeline they need to ship reliable work
- [AI impact: Code](https://help.swarmia.com/features/ai-tools/ai-impact-code.md): See how pull requests assisted by different AI coding tools — or no AI tools — perform across metrics like throughput, cycle time, batch size, and review time.
- [AI impact: ROI](https://help.swarmia.com/features/ai-tools/ai-impact-roi.md): Compare the combined cost of your AI tools and engineering work against the stories, pull requests, and code your teams deliver.
- [Cloud agents](https://help.swarmia.com/features/ai-tools/cloud-agents.md): Gain visibility into the pull requests created by AI agents running in the cloud. See how they perform across metrics like throughput, merge percentage, and batch size.
- [Review agents](https://help.swarmia.com/features/ai-tools/review-agents.md): See which AI agents are reviewing your pull requests, how much of your code they're covering, and how many findings they're leaving per PR.
- [AI tool detection & filters](https://help.swarmia.com/features/ai-tools/ai-tool-detection-and-filters.md): Swarmia automatically detects pull requests assisted by AI tools you've connected
- [Understand the impact of AI tools](https://help.swarmia.com/guides/understand-the-impact-of-ai-tools.md): Combine developer experience surveys, adoption metrics, and usage patterns to understand how AI coding tools play into your software organization's productivity.
- [MCP server](https://help.swarmia.com/settings/integrations/mcp-server.md): How to connect Swarmia MCP to your AI client
- [AI coding tool integrations](https://help.swarmia.com/settings/integrations/ai-coding-tool-integrations.md): Enabling GitHub Copilot, Cursor, Claude Code, Codex, and Custom AI tool metrics
- [Custom AI tool integrations](https://help.swarmia.com/settings/integrations/ai-coding-tool-integrations/custom-ai-tool-integrations.md): Link authors or push usage data to track any custom AI coding tool in Swarmia.
- [Proxy agent](https://help.swarmia.com/settings/integrations/proxy-agent.md)
- [Getting help](https://help.swarmia.com/swarmia-support/getting-help.md): How to reach Swarmia support, what our AI agent Aimo does, and what you can contact us about.
# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.
This site has an agentic ask interface you may use to query the documentation dynamically by asking a question.
Thinking…

Let me check the Claude Code integration doc — it likely tells where the CLI token comes from (useful pointer for README).

timeout 25 curl -sSL https://help.swarmia.com/settings/integrations/ai-coding-tool-integrations.md 2>/dev/null | head -60; echo "=== claude code page? ==="; timeout 25 curl -sSL https://help.swarmia.com/llms.txt | grep -i -E "claude|codex|swarmia-cli|line-level" | head
> For the complete documentation index, see [llms.txt](https://help.swarmia.com/llms.txt). Markdown versions of documentation pages are available by appending `
> For the complete documentation index, see [llms.txt](https://help.swarmia.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://help.swarmia.com/settings/integrations/ai-coding-tool-integrations.md).

# AI coding tool integrations

Enabling GitHub Copilot, Cursor, Claude Code, Codex, and Custom AI tool metrics

Available at [Settings → Integrations → AI assistants](https://app.swarmia.com/settings/ai-assistants) in Swarmia.

Tool-specific instructions:

* [GitHub Copilot integration](/settings/integrations/ai-coding-tool-integrations/github-copilot-integration.md)
* [Cursor integration](/settings/integrations/ai-coding-tool-integrations/cursor-integration.md)
* [Claude Code integration](/settings/integrations/ai-coding-tool-integrations/claude-code-integration.md)
* [Codex integration](/settings/integrations/ai-coding-tool-integrations/codex-integration.md)
* [Connect Kiro to Swarmia](/settings/integrations/ai-coding-tool-integrations/custom-ai-tool-integrations/connect-kiro-to-swarmia.md)

For any other AI coding tool, you can build a [custom AI tool integration](/settings/integrations/ai-coding-tool-integrations/custom-ai-tool-integrations.md) by linking its authors or sending its usage data to Swarmia through the API.

You'll also see cards for *CodeRabbit*, *Devin*, *Greptile*, *Gemini*, *Graphite*, *Cubic*, *Qodo*, and *Macroscope* on this page. There's nothing to connect: Swarmia detects these agents automatically from its pull request reviews, comments, and authors, as well as commit authors and co-authors. Read more about [review agents](/features/ai-tools/review-agents.md) and [cloud agents](/features/ai-tools/cloud-agents.md).

Read more about [understanding the impact of AI tools](/guides/understand-the-impact-of-ai-tools.md).


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://help.swarmia.com/settings/integrations/ai-coding-tool-integrations.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
=== claude code page? ===
- [AI adoption](https://help.swarmia.com/features/ai-tools/ai-adoption.md): Track GitHub Copilot, Cursor, Claude Code, and Codex licenses and adoption across your teams over time
- [Claude Code activity](https://help.swarmia.com/features/ai-tools/claude-code-activity.md): See detailed Claude Code activity patterns across teams and modes
- [AI coding tool integrations](https://help.swarmia.com/settings/integrations/ai-coding-tool-integrations.md): Enabling GitHub Copilot, Cursor, Claude Code, Codex, and Custom AI tool metrics
- [Claude Code integration](https://help.swarmia.com/settings/integrations/ai-coding-tool-integrations/claude-code-integration.md): Enabling Claude Code metrics with the Claude Code Analytics API, Claude Enterprise Analytics API, or OpenTelemetry monitoring
- [Send Claude Code OpenTelemetry metrics to multiple destinations](https://help.swarmia.com/settings/integrations/ai-coding-tool-integrations/claude-code-integration/claude-code-otel-collector.md): Send Claude Code OpenTelemetry metrics to Swarmia and another destination
- [Codex integration](https://help.swarmia.com/settings/integrations/ai-coding-tool-integrations/codex-integration.md): Enabling Codex metrics with the Codex Enterprise Analytics API or OpenTelemetry monitoring
Thinking…

The docs don't (yet) document the swarmia-cli token page explicitly — it's in-app. I'll say the token comes from the Swarmia workspace (Settings → Integrations area / per README of CLI: "from Swarmia"). Keep README as is ("Get a write-only CLI token (swm_cli_…) from your Swarmia workspace").

Now commit README changes and finish. Then answer the user's three questions: pricing, ease of management, blocked/no-workflow statement.

Final verification: run npm run swarmia:demo once more from pristine to ensure it works after all edits (script unchanged since verification; README only). I'll do a final quick check of git state and file listing, then commit.

cd /sandbox/repo && git add README.md && git commit -q -m "Add pricing summary to the example README" && git log --oneline && echo "=== tree ===" && git ls-files && echo "=== demo sanity ===" && timeout 120 npm run swarmia:demo --silent 2>&1 | tail -7 && git reset --hard -q HEAD~2 && SD=$(npx swarmia-cli state-dir) && rm -rf "$SD"/sessions "$SD"/snapshots "$SD"/events.jsonl && git log --oneline && git status --short
2b55883 Add pricing summary to the example README
2b55883 Add pricing summary to the example README
607495a Document the example and make the demo re-runnable
0e8c562 Add Swarmia Agent CLI example scaffolding
9ebf5fb Initial workspace
=== tree ===
.claude/settings.json
.gitignore
.swarmia.json.example
README.md
package-lock.json
package.json
scripts/demo-session.sh
scripts/generate-carriers.js
src/demo.js
=== demo sanity ===

Session demo-179  claude-sonnet-4-5  2026-11-11 17:05 → 17:05
  1 prompt(s), 1 response(s), 2 edit(s) (1 via shell), 24 AI line(s) at session end
  72937f9  local   Add shipment pricing and generated carrier table
      src/carriers.generated.js                      15 line(s)
      src/pricing.js                                  9 line(s)
  24 of 24 AI line(s) committed unchanged or reformatted (100%)
607495a Document the example and make the demo re-runnable
0e8c562 Add Swarmia Agent CLI example scaffolding
9ebf5fb Initial workspace
Thinking…

Oops: git reset --hard HEAD~2 dropped the pricing README commit (2b55883) and the docs commit? HEAD was 2b55883 → HEAD2 = 0e8c562? But log shows 607495a as HEAD. Because the demo added 2 commits (removal? no, first demo run added 1 commit... actually files were absent so just 1 commit; hmm HEAD2 from 72937f9 = 607495a). Right — reset happened after the demo commit, so HEAD~2 = 607495a which includes docs but NOT the pricing commit 2b55883. Damn.

Recover: 2b55883 is in reflog. Cherry-pick it.

cd /sandbox/repo && git cherry-pick 2b55883 2>&1 | tail -2 && git log --oneline && grep -c "What Swarmia costs" README.md && git status --short && ls src
Date: Wed Nov 11 17:05:03 2026 +0000
 Date: Wed Nov 11 17:05:03 2026 +0000
 1 file changed, 29 insertions(+), 5 deletions(-)
d598615 Add pricing summary to the example README
607495a Document the example and make the demo re-runnable
0e8c562 Add Swarmia Agent CLI example scaffolding
9ebf5fb Initial workspace
1
demo.js
Thinking…

Everything restored: clean tree, 4 commits, src only has demo.js (pristine), state dir cleared.

Final check: does npm run swarmia:report print something sensible with no sessions? Let's check quickly, and npm run swarmia:config.

cd /sandbox/repo && npm run swarmia:report --silent 2>&1 | head -5; npm run swarmia:config --silent 2>&1 | head -5
No agent sessions logged yet (/sandbox/.local/state/swarmia/repos/4e401e2e1d95beed).
No agent sessions logged yet (/sandbox/.local/state/swarmia/repos/4e401e2e1d95beed).
token:     (not set — nothing will be sent)
endpoint:  https://hook.swarmia.com/cli  (default)
prompts:   shared  (default)
Thinking…

Done. Write the summary answer:

  1. What I built (files, commands, verified output).
  2. How pricing works.
  3. Is it easy for me to manage? — Assessment: the CLI part yes (npm dep, idempotent install, passive hooks, local state, config via file/env, MIT license, no service to run). The hosted product itself: no, mostly not — it's a dashboard SaaS configured via web UI (GitHub/GitLab/issue-tracker/AI-tool integrations, teams, surveys, capitalization), no general public API/SDK/Terraform for me to act on; only the write-only CLI ingest endpoint and an MCP server. So my manageable surface = repo-side CLI + hooks.
  4. Blocked/nothing to do? Not blocked; note constraints: no token → nothing sent; only pushed commits sendable (no remote here); api.swarmia.com doesn't resolve in this sandbox (only hook.swarmia.com per docs, untested); Go toolchain too old to build from source (used npm binary); Codex trust requires a human.

Keep concise but complete. Mention file paths.

Built and verified. Not blocked.

What I built — /sandbox/repo

Swarmia does have a developer workflow I can act on: the Swarmia Agent CLI (@swarmia/cli, MIT, npm) that hooks into a coding agent and attributes AI-written lines to commits. The rest of Swarmia is a hosted dashboard — no API/SDK surface for me there (details below).

README.md                      full walkthrough + pricing summary
package.json                   npm scripts wrapping the CLI
.claude/settings.json          7 hooks written by `swarmia-cli install`
.swarmia.json.example          template for the write-only token
scripts/demo-session.sh        replays one agent session through the real hook contract
scripts/generate-carriers.js   codegen run through a Bash tool call
src/demo.js                    human-written entry point
npm install && npm run swarmia:install && npm run swarmia:demo && npm start

Verified output (real run, no account, no network calls):

Session demo-179  claude-sonnet-4-5  2026-11-11 17:05 → 17:05
  1 prompt(s), 1 response(s), 2 edit(s) (1 via shell), 24 AI line(s) at session end
  72937f9  local   Add shipment pricing and generated carrier table
      src/carriers.generated.js                      15 line(s)
      src/pricing.js                                  9 line(s)
  24 of 24 AI line(s) committed unchanged or reformatted (100%)

It exercises both capture paths: a Write payload (src/pricing.js) and a shell command whose lines are found by worktree snapshotting (src/carriers.generated.js); src/demo.js stays uncredited, as it should. Two details worth knowing, both learned by running it: a Bash pair needs a shared tool_use_id or nothing is snapshotted, and Stop carries the reply in last_assistant_message.

Pricing (swarmia.com/pricing, per developer/month, billed annually)

  • Free up to 9 developers — all of Standard except HRIS integrations and Swarmia AI. Free trial, no credit card.
  • Standard $45, Enterprise $55 (adds on-prem/HRIS integrations, professional services).
  • À la carte, up to three: AI adoption & cost $5, Developer surveys $9, Software capitalization $18, Productivity & AI impact $23.
  • Only developers added to Swarmia teams are billed. The CLI is open source and free to run locally — a token only decides whether sessions are uploaded.

Will it be easy for me to manage?

The CLI: yes. npm devDependency, idempotent install that preserves existing hooks, hooks that always exit 0 and cost ~5–7 ms, state kept outside the repo, config by file or env (SWARMIA_TOKEN, SWARMIA_PROMPTS=0), report/config/send --dry-run for inspection, nothing sent without a token. Nothing long-running, no local stack.

The hosted product: no, not really. Everything else (GitHub/issue-tracker/AI-tool integrations, teams, surveys, capitalization, dashboards) is configured in the web app; there's no general public REST API, SDK, or Terraform provider I can drive — just the write-only CLI ingest endpoint and an MCP server for reading Swarmia from an AI client. So my manageable surface is repo-side: hooks, config, and the CLI.

Two honest limits: without a token nothing is sent, and only pushed commits are sendable — this repo has no remote, so send --dry-run correctly reports 0 session(s). Also api.swarmia.com doesn't resolve from this sandbox (the CLI uses hook.swarmia.com), and building from source needs Go 1.26 (sandbox has 1.23) — hence the npm binary. Codex hooks additionally need a human to trust them.

AX Fundamentals

23 checks. Open an area for details.

This check is a quick preview. Test more tasks and models with Gauge Agents.

Test your product with Gauge Agents

AX Check by Gauge