{"slug": "actualis-read-what-your-coding-agent-did-on-your-machine", "title": "Actualis – read what your coding agent did on your machine", "summary": "Actualis, a new open-source tool from Digital Foundry, reads coding-agent session logs on a user's machine to report shell commands, credential exposure, refused tool calls, and per-project costs across Claude Code and Codex, with no account, config, or network required. The tool claims to correct cost overcounting by 2.13× and verifies its own claims via a self-check, and is available as a single-file AGPL-3.0 Python script with no third-party dependencies.", "body_md": "**What actually ran.**\n\n[actualis.app](https://actualis.app) · [Latest release](https://github.com/digital-foundry/actualis/releases/latest) · [Changelog](/digital-foundry/actualis/blob/main/CHANGELOG.md) · [Security policy](/digital-foundry/actualis/blob/main/SECURITY.md) · [Trade marks](/digital-foundry/actualis/blob/main/TRADEMARKS.md)\n\nYour coding agent writes down everything it did — every shell command, every\ntoken, every refused tool call — and then nothing reads it. `actualis`\n\nreads it.\n\n```\nuv tool install actualis     # or: pipx install actualis\nactualis\n```\n\nNo account, no config file, no network. It reads files already on your disk\nand prints a report, across **Claude Code** and **Codex** together.\n\n**What it finds that you probably don't know:**\n\n**Credentials that ended up in shell commands** your agent ran, grouped by fingerprint and ranked for rotation. The value itself is never printed or stored — only a hash of it.**Every command the agent ran**, audited for the risky shapes:`rm -rf`\n\n, piped installers, credential reads, egress to somewhere new.**What was refused, and by whom**— you, or the auto-approval policy. A refused command is never sent to a provider, so nothing watching the API can see it. It exists only on your disk.**What it cost**, per project, per model, per ticket — counted once per billable message, not once per transcript record. A transcript re-emits the same assistant record while a response streams; counting those repeats overstated my own fleet's spend by 2.13×.\n\n```\nactualis --self-check\n```\n\nThe claims above are the product, so the tool verifies them on your machine\ninstead of asking you to believe them: which modules the shipped source\nimports at any depth, your transcripts hashed before and after a real scan to\nshow they are byte-identical, nothing created or deleted, the only path it can\nwrite to, and its own sha256 to compare against the published wheel. It also\nprints what it does **not** prove. [More on privacy](#privacy).\n\nOne file, no third-party dependencies, AGPL-3.0. If you are about to point something at your session history, you should be able to read it in a sitting.\n\nIllustrative output from a synthetic fleet — every project, branch and\ncredential above is invented. Regenerate with tools/make-demo-fleet.py.\n\n``` bash\n$ actualis\n\nFLEET ──────────────────────────────────────────────────────────────\n  window        2026-05-04 → 2026-06-04  (31 days)\n  transcripts   168 files, 0.4 GB\n  messages      38,204\n  cost          $12,480.55 notional, at API list price\n  per active day $402.60   ·  per week $2,818.20   ·  31 active days of 31\n\nBY PROJECT ─────────────────────────────────────────────────────────\n    $10,159.17   81.4% ███████████████████████████ web-app\n     $1,385.34   11.1% ███ api-service\n       $87.36    0.7%  data-pipeline\n\n  ▲ 81% of all spend is one project: web-app\n\nSHELL AUDIT ────────────────────────────────────────────────────────\n  bash calls    13,006  73% of all agent tool calls\n  permission    auto=7,140  default=402  acceptEdits=377  plan=14\n  denied        automode-blocked=58  user-rejected=31\n\n  ▲ 1,315 commands contained credential material\n  flagged   340 high   148 medium   of 13,006 commands\n```\n\nTerminal-native coding agents write a complete record of every session to your disk: token usage per turn, every tool call, every shell command. What they don't give you is a view across all of it. If you run agents in more than one project, or more than one agent, you cannot currently answer:\n\n- What did my agents cost last month?\n- Which project is burning the budget?\n- What did issue #412 cost?\n- What shell commands have my agents actually been running?\n- Did a credential ever end up in a command?\n\n`actualis`\n\nanswers all five from data already on your machine, in one report.\n\nNo dependencies beyond Python 3.9+. Either run the file directly:\n\n```\npython3 actualis.py\n```\n\nOr install it as a command:\n\n```\nuv tool install .      # or: pipx install .\nactualis\n```\n\n`uv tool install`\n\ncopies the code, so re-run it with `--force`\n\nafter pulling to\npick up changes.\n\n```\npython3 actualis.py                  # full report\npython3 actualis.py --days 30        # last 30 days\npython3 actualis.py --bash           # shell audit only\npython3 actualis.py --coach          # findings and recommended actions only\npython3 actualis.py --watch          # live alerting on new secrets\npython3 actualis.py --project svc    # filter to matching projects\npython3 actualis.py --json           # machine-readable\npython3 actualis.py --top 25         # show more projects\npython3 actualis.py --agent codex    # one agent only (claude | codex | all)\n```\n\n| flag | effect |\n|---|---|\n`--days N` |\nonly the last N days |\n`--project SUBSTR` |\nonly projects whose name contains SUBSTR |\n`--top N` |\nhow many projects and tickets to list (default 12) |\n`--agent {all,claude,codex}` |\nwhich agents to include (default all) |\n`--root DIR` |\nread one specific transcript directory instead of discovering them |\n`--bash` |\nshell audit only |\n`--coach` |\nfindings and actions only |\n`--share` |\npostable summary with nothing identifying in it |\n`--json` |\nmachine-readable (\n|\n\n`--diff OLD.json`\n\n`--json`\n\nreport: what appeared, what went away, what got worse`--watch`\n\n`--interval SEC`\n\n`--watch`\n\npoll interval, default 4`--quiet`\n\n`--watch`\n\n: notify on secrets only, not every flagged command`--no-redact`\n\n**do not** redact credentials from output; unsafe to share`--suppress ID`\n\n`--reason TEXT`\n\n`--suppressions`\n\n`--fail-on LEVEL`\n\n`critical`\n\n, `high`\n\nor `any`\n\n. For gating a pipeline`--explain [TOPIC]`\n\n`--why AFxxx`\n\n`--agents`\n\n`--mcp`\n\n[below](#ask-the-agent-about-itself))`--service KIND`\n\n`launchd`\n\n, `systemd`\n\nor `newsyslog`\n\nunit for `--watch`\n\n, paths already resolved`--self-check`\n\n`--root`\n\nand `--days`\n\n`--completions SHELL`\n\n`bash`\n\n, `zsh`\n\nor `fish`\n\n`--version`\n\nThe script is generated from the parser, so it never drifts from the flags this\nbuild actually has. `--explain`\n\n, `--why`\n\n, `--agent`\n\nand `--fail-on`\n\ncomplete\ntheir real values.\n\n```\n# zsh — any directory on your fpath\nactualis --completions zsh > ~/.zsh/completions/_actualis\n\n# bash\nactualis --completions bash > ~/.local/share/bash-completion/completions/actualis\n\n# fish\nactualis --completions fish > ~/.config/fish/completions/actualis.fish\n```\n\nRegenerate after upgrading. `--suppress`\n\nis deliberately not completed: its\nvalues are finding ids from your own report, and producing them needs a full\nscan — a tab key that hangs the terminal is worse than one that does nothing.\n\n`FLEET`\n\ntotals and sources · `TOKENS`\n\nbroken out by cache bucket with the\nmultiplier applied to each · `BY AGENT`\n\n· `BY MODEL`\n\n· `CACHE EFFICIENCY`\n\n·\n`BY TICKET`\n\n· `TOOL CALLS`\n\n· `SUBAGENTS`\n\n· `SHELL AUDIT`\n\n· `COACH`\n\n.\n\n|\n\n`AF001`\n\n–`AF011`\n\n: what it means, when it fires, what to do[docs/secrets.md](/digital-foundry/actualis/blob/main/docs/secrets.md)[docs/json.md](/digital-foundry/actualis/blob/main/docs/json.md)`--json`\n\nschema[CONTRIBUTING.md](/digital-foundry/actualis/blob/main/CONTRIBUTING.md)[SECURITY.md](/digital-foundry/actualis/blob/main/SECURITY.md)[CHANGELOG.md](/digital-foundry/actualis/blob/main/CHANGELOG.md)Branch names almost always carry the issue number, so the same data that answers\n\"what did this project cost\" also answers **\"what did issue #412 cost\"** — the\nunit engineering and finance already budget in.\n\n```\nBY TICKET  (top 5 of 58)\n         cost  ticket         msgs   days  where\n    $1,884.10  #412        3,110      5  feat/412-checkout-v2, feat/412-checkout-api +1\n    $1,102.40  #310         1,240      2  fix/310-session-timeout\n    $980.25  #907         1,206      2  feat/907-export-queue\n\n  $8,140.20 across 58 tickets (12 spanning several branches) · $3,890.15 on trunk\n```\n\nOne ticket often spans several branches, so grouping by ticket rather than branch\nis the point. `feat/412-p4-…`\n\n, `p5-…`\n\nand `p6-…`\n\nare one number. Work on trunk or\nin a detached HEAD is reported separately rather than guessed at.\n\nRecognised: `feat/412-slug`\n\n, `fix/310-slug`\n\n, `PROJ-456`\n\n, `feature/PROJ-456`\n\n,\n`issue-742`\n\n, `gh_91`\n\n, `412-slug`\n\n. Anything else is left unattributed rather than\ninvented.\n\nThe report says what happened; `--coach`\n\nsays what to do about it. Findings carry\nstable ids (`AF001`\n\n–`AF010`\n\n) so they can be quoted and documented, and each one\ncarries evidence, an action, and an impact estimate where one can be computed\nhonestly.\n\n**Benchmarks are computed against you, not against other users.** Project versus\nproject, week versus week, ticket versus your median ticket. That needs no\ntelemetry, no account, and no population — it works on day one with one user, and\nit keeps the no-network promise intact.\n\nFindings are earned. On a fleet with nothing notable, the coach prints nothing.\n\nFull reference: [docs/findings.md](/digital-foundry/actualis/blob/main/docs/findings.md).\n\n```\nCACHE EFFICIENCY\n  fleet hit rate  98.1% of input context served from cache\n  saved           $71,905.40 versus sending the same context uncached\n\n   hit rate         context        saved  project\n      97.4%  14,220,551,900  $58,110.20  web-app\n      96.9%   2,140,882,003   $8,795.15  api-service\n      96.1%     412,660,004    $1,102.30  data-pipeline\n\n  No project is more than 15 points below your median of 96.1%.\n```\n\nHit rate is `cache_read / (input + cache_write + cache_read)`\n\n— the share of\n**input context** served from cache. Output tokens are excluded because they are\nnot cacheable, and including them makes a chatty project look broken when its\ncaching is fine.\n\nSavings are measured against the counterfactual of sending the same context\nuncached, priced per model at the message level. Note that a project doing mostly\ncache *writes* can show negative savings, since a 1-hour write costs 2.00x. That\nis reported rather than clamped to zero.\n\nA project more than 15 points below your own median is flagged (`AF002`\n\n) as\nlikely having something unstable early in its prompt prefix. Projects below the\nreporting threshold are excluded from both the table and the coach, so the two\nnever disagree.\n\nSubagent runs are reported separately: how many, which models, how much shell and edit activity, wall-clock, and lines changed.\n\n```\nSUBAGENTS\n  214 runs · 18.4 hours wall-clock · 38,910 lines added, 6,204 removed\n       151  claude-sonnet-5\n        34  claude-haiku-4-5\n        22  claude-opus-4-8[1m]\n\n  tool activity  bash 3,402 · read 1,188 · edit 820\n  cost floor     $16.44 — a LOWER BOUND, excluded from the headline figure\n```\n\n**Their cost is a floor, not a total, and it is kept out of the headline number.**\nThe parent transcript records only each run's final message: `totalTokens`\n\nequals\nthe sum of that single `usage`\n\nobject in 873 of 873 observed cases, and scales\nabout 2x from a 4-tool run to a 45-tool run, which is context growth rather than\nsummation. The cumulative spend of a subagent's turns is not recoverable, so it is\nnot estimated.\n\n**The bigger finding is what the audit cannot see.** 3,402 shell commands ran\ninside subagents — 21% of all shell activity — and their command text is never\nwritten to the parent transcript. Subagents inherit the parent's permissions but\nnot its visibility. The shell audit says so explicitly rather than reporting a\nnumber that looks complete.\n\n`--share`\n\nprints a postable summary containing nothing that identifies you: no\nproject names, branches, ticket ids, paths, commands, or fingerprints. Only\ntotals, rates, distributions, and generic finding titles.\n\n```\n  actualis · what my coding agents cost and did\n\n  31 active days   2 agent(s)   38,204 messages   17,540,882,110 tokens\n\n  $12,480.55 at API list price   ·   $402.60/active day\n  98.1% of input context from cache, saving $71,905.40 against sending it uncached\n  81% of spend in a single project\n  $41.20 median cost per ticket, over 58 tickets\n\n  13,006 shell commands   73% of all tool calls\n  92% of turns ran unsupervised\n  21% of shell activity happened inside subagents, where commands are not recorded\n  19 distinct credentials found in command history   6 critical, 18 worth rotating\n\n  coach   AF004  AF003  AF005  AF011  AF001  AF007  AF008  AF009\n```\n\nThe test suite plants identifying strings — a project name, a branch, a path, a live-shaped key, an internal hostname — and asserts that none of them can reach this output. Secret fingerprints are excluded too, since a hash is still an identifier that could be correlated.\n\nEvery figure is answerable: where it came from, how it was computed, what it\nassumes, and how to check it **without trusting this tool**.\n\n```\nactualis --explain            # list the topics\nactualis --explain cost       # the formula, the assumptions, an independent check\nactualis --why AF005          # why one finding fired, with your actual numbers\n```\n\nTopics: `sources`\n\n, `cost`\n\n, `cache`\n\n, `tickets`\n\n, `secrets`\n\n, `subagents`\n\n, `shell`\n\n,\n`coach`\n\n, `agents`\n\n.\n\nEach explanation carries the same four parts, deliberately: what it measures, the exact formula, what it assumes, and a command that checks the answer some other way. If a number cannot be interrogated, it should not be acted on.\n\nThis tool reads what agents did. The obvious next question is whether the agent\nitself is genuine — a modified `claude`\n\nbinary could do anything and still write\na plausible transcript.\n\n``` bash\n$ actualis --agents\n\n  OK   Claude Code  claude\n       Developer ID Application: Anthropic PBC (Q6L2SF6YDW)\n       signature valid, team Q6L2SF6YDW as expected\n\n  OK   Codex  codex\n       Developer ID Application: OpenAI OpCo, LLC (2DC432GLL2)\n       signature valid, team 2DC432GLL2 as expected\n\n  -    GitHub Copilot CLI  copilot\n       no code signature (expected for npm and script installs)\n```\n\n| status | meaning |\n|---|---|\n`OK` |\nvalidly signed by the publisher expected for that tool |\n`WARN` |\nvalidly signed, but not by the expected publisher |\n`FAIL` |\nsignature present and invalid — the binary was modified |\n`-` |\nunsigned; normal for npm and script installs |\n`?` |\nsigned by an unpinned publisher, or unassessable on this platform |\n\nTeam IDs are pinned per tool, so a valid signature from the *wrong* publisher is\nvisible rather than silently accepted.\n\n**What a valid signature proves:** the binary came from that publisher and has\nnot been altered since signing. Tested by flipping one byte in a 325 MB signed\nbinary; it reports `FAIL`\n\n. **What it does not prove:** that the software is safe,\nor that the publisher deserves trust. **Unsigned is not malicious** — script\nbased tools are never code-signed.\n\nmacOS only. Other platforms report *unassessed* rather than pretending.\n\n`--mcp`\n\nruns an MCP server over stdio, so the agent producing the data can query\nit mid-session: *\"what did this ticket cost?\"*, *\"do I have credentials\nexposed?\"*\n\n```\nclaude mcp add actualis -- actualis --mcp\n```\n\nFive tools: `fleet_summary`\n\n, `ticket_cost`\n\n, `exposed_secrets`\n\n, `coach_findings`\n\n,\n`shell_audit`\n\n.\n\nNo port, no daemon, no network — stdio only, and the same read-only local scan as everything else. Implemented against the standard library rather than the MCP SDK, because a tool whose pitch is \"one auditable file, no supply chain\" cannot take a dependency to speak line-delimited JSON.\n\n**Everything it returns is written back into a transcript** that this tool then\nscans, so the surface is deliberately narrow: aggregates, types, fingerprints and\ncounts. Never a secret value, and never raw command text.\n\nThe scan is cached for the life of the process, since a large fleet takes about a minute to read.\n\nNothing leaves your machine. No network calls, no telemetry, no analytics, no\nconfig file, no writes. It opens files under `~/.claude/projects`\n\nread-only and\nprints to stdout. The whole program is one readable file; if you're about to point\na tool at your session history, you should be able to audit it in a sitting, so it\nwas written to be read.\n\nThose are claims, so the tool checks them for you rather than asking you to take them on faith:\n\n```\nactualis --self-check\n```\n\nIt reads its own source and reports every module it imports (a Python process\ncannot open a network connection without `socket`\n\n), hashes a sample of your\ntranscripts before and after a real scan to show they are byte-identical,\nconfirms no file appeared or vanished under the transcript roots, names the only\npath it can ever write to, and prints its own sha256 so you can compare it with\nthe published wheel. It exits non-zero if any of that fails.\n\nPassing is a floor, not a guarantee, and the output says so: it proves what this\nrun did, not what every run could do. The stronger check is still to watch the\nprocess yourself, and `--self-check`\n\nprints the command for your platform.\n\nEvery transcript directory it scanned is printed in the report header. It checks\n`~/.claude/projects`\n\n**and** `$CLAUDE_CONFIG_DIR/projects`\n\n, because a machine can\nhave both, and a fleet report that silently covers half your fleet is worse than\nno report.\n\nCosts are Anthropic API list prices, verified 2026-08-22, including the cache multipliers that dominate agent workloads:\n\n| multiplier on input rate | |\n|---|---|\n| cache read | 0.10× |\n| cache write, 5m TTL | 1.25× |\n| cache write, 1h TTL | 2.00× |\n\nThis matters more than it sounds. On a typical agent workload **97% of all tokens\nare cache reads**, so any tool that prices them at the input rate will overstate\nyour spend by roughly an order of magnitude.\n\n**If you're on a Pro or Max subscription, this is not a bill.** Your actual outlay\nis the flat subscription fee. Read the total as *what this would have cost at API\nlist price*: an opportunity-cost figure, a consumption signal, and a way to see\nwhich project is eating your quota. Models with no published rate are priced at\nthe top of the known range for their provider, and that share is reported as its\nown number so you can subtract it rather than having to trust it.\n\n**One message is counted once.** A transcript repeats the same assistant record\nwhile a response streams — identical message id, identical usage block, a fresh\nrecord uuid each time — so the number of records is not the number of messages.\nVersions before 0.1.1 billed every record. On a real corpus of 145,116 usage\nrecords, 50.9% were repeats and the total came out **2.13× too high**: $46,997\nreported against $22,064 actual. The report prints how many repeats it collapsed,\nso you can see the deduplication working rather than take it on faith. **If you\nhave a figure from 0.1.0, re-run it.**\n\n72% of what a coding agent does is run shell commands. That is the largest surface\nby far, and it's the one thing an MCP gateway structurally cannot see, because a\ngateway sits between the agent and MCP servers and never observes a local `Bash`\n\ncall.\n\nThe audit is **deterministic**. Plain pattern matching, no model in the loop, no\nscoring that drifts between runs. A command either matches a rule or it doesn't,\nand you can read every rule in the source. Categories: `destructive`\n\n, `privilege`\n\n,\n`remote-exec`\n\n, `credentials`\n\n, `egress`\n\n, `git`\n\n, `publish`\n\n, `database`\n\n, `audit`\n\n.\n\n**A flag means \"worth looking at\", not \"wrong\".** Most `rm -rf`\n\ncalls are a build\ndirectory. The point is that you can see them at all.\n\nThe rules were tuned against 48,000 real agent commands, and tuning meant deleting\nrules as much as adding them. A rule matching `>/dev/null 2>&1`\n\nas \"audit\ntampering\" fired 1,206 times at essentially 100% false positive, so it's gone; a\nnoisy rule destroys trust in the rules that matter. Current flag rate is about 3.8%.\n\nAn exit code is the smallest possible integration, and it fits the read-only promise exactly: the tool returns a verdict and still changes nothing.\n\n```\nactualis --days 7 --fail-on critical\n```\n\n| exit | means |\n|---|---|\n`0` |\nnothing at or above the threshold |\n`1` |\ncould not run: no transcripts, an unreadable `--root` |\n`2` |\na command-line usage error (argparse's, not ours) |\n`3` |\nfindings at or above `--fail-on` |\n`130` |\ninterrupted |\n\nFindings are **3**, not 1 and not 2. `1`\n\nalready meant \"could not run\", and `2`\n\nis what argparse returns for a bad invocation — a pipeline that cannot tell *a\ncredential is exposed* from *you mistyped a flag* will eventually be told to\nignore both.\n\nThe verdict goes to **stderr**, so `--json`\n\non stdout stays byte-identical and a\npipeline can capture the report and the outcome separately:\n\n```\nactualis --json --fail-on high > report.json || echo \"gate failed\"\n```\n\n**Suppressed findings do not fail the build.** That is what suppression is for —\nif a recorded, reasoned decision still broke CI, people would delete findings\ninstead of suppressing them. They remain counted in the report, and coach\nfindings derived from a suppressed credential are suppressed with it.\n\nA detector that cries wolf gets ignored, so there is a way to tell it it is wrong, at the point where you disagree with it rather than in documentation you would have to go looking for:\n\n```\nactualis --suppress a41f9c02 --reason \"test fixture in our CI config\"\nactualis --suppressions\n```\n\nSuppressions are a plain text file — greppable, diffable, reviewable in a pull\nrequest, and editable by hand six months later by someone who did not write it.\nRead from `$XDG_CONFIG_HOME/actualis/suppressions`\n\nand from\n`./.actualis-suppressions`\n\n, so a team can commit a shared list.\n\n**A suppression never removes a finding from the count.** It is held back from\nthe actionable list, and it still appears in `--json`\n\nwith `suppressed: true`\n\nand\nits reason. If suppressing something deleted it, the report would start lying by\nomission and you could not tell a clean scan from a heavily suppressed one.\n\nIf a detection is wrong for everyone rather than just for you, the report prints a pre-filled issue URL. It prints it; it never opens it and never sends anything.\n\n**Credentials are redacted from all output by default**, including `--json`\n\n.\n\nAgent transcripts contain live secrets. This is not hypothetical: the first real\nrun of this tool surfaced a live deployment token sitting in plaintext in a saved\nsession. Since the output of a reporting tool gets pasted into issues, dropped into\nchat, and screenshotted, redaction is the default and `--no-redact`\n\nis an explicit\nopt-out that prints a warning.\n\nFull list of what is and is not detected: [docs/secrets.md](/digital-foundry/actualis/blob/main/docs/secrets.md).\n\nRedaction covers `KEY=value`\n\nfor secret-shaped names, ~25 known token prefixes\n(`ghp_`\n\n, `sk-ant-`\n\n, `AKIA`\n\n, `vcp_`\n\n, `glpat-`\n\n, …), `Authorization:`\n\nheaders, and\npasswords in connection URLs. Shell variable *references* like `$VERCEL_TOKEN`\n\nare\nleft readable, because the reference isn't the secret and masking it only makes the\noutput harder to read. Redaction is idempotent.\n\nIf the report tells you commands contained credential material, those secrets are sitting in plaintext in your transcripts. Rotate anything live.\n\n| Agent | Supported | Why |\n|---|---|---|\nClaude Code |\nyes | `~/.claude/projects/**/*.jsonl` |\nCodex |\nyes | `$CODEX_HOME/sessions/**/rollout-*.jsonl` |\n| Cursor | no |\nNothing to read. All `composerData` records are empty shells: `conversationMap {}` , `usageData {}` . The `ai_code_hashes` and `conversation_summaries` tables have zero rows. Content is server-side. |\n| Windsurf | no |\n`globalStorage` holds config and auth only. No conversation or usage store. Server-side. |\n| Cline, Aider | not yet | Both write local files. Untested, likely feasible. |\n\nThe pattern is clean: **terminal-native agents write local rollouts, IDE forks are\nthin clients that keep everything server-side.** Supporting Cursor or Windsurf would\nmean network calls and OAuth against their APIs, which would cost this tool the three\nproperties it's built on — no network, read-only, auditable in one sitting. That\ntrade isn't worth making, so the scope is stated honestly instead: every agent with\na shell on your machine.\n\nTwo provider quirks the cost code has to get right, because both silently overcharge if handled like the other:\n\n**Anthropic** reports`input_tokens`\n\n*excluding*cache, with cache reads and writes as separate buckets.**OpenAI** reports`input_tokens`\n\n*including*`cached_input_tokens`\n\n, and`reasoning_output_tokens`\n\nas a subset of`output_tokens`\n\n. Neither is an addition.- Codex's\n`total_token_usage`\n\nis**cumulative** across a session and its`token_count`\n\nevents repeat, so the session total is the final value, never a sum.\n\n**Reporting only.** It observes; it does not enforce. Claude Code's own permission rules, sandboxing, and hooks are where enforcement belongs.**Pattern matching has a ceiling.** A command that builds a string dynamically, or runs a script whose contents live in a file, will not be caught. This raises the floor on visibility; it is not a security boundary.**Prices are hardcoded** and dated in the source. They will drift. OpenAI rates come from a third-party aggregator rather than OpenAI's own page.**Deduplication is by message id.** A record with no id cannot be keyed and is always counted, so a transcript format that stops emitting ids would silently return to over-counting. A repeat count of zero on a large scan is the signal that this has happened.**Rates use active days**, not calendar span, so one stale session from months ago doesn't silently divide your weekly burn rate by five.`--days N`\n\ncovers the last N calendar days including today, in UTC, so active days can never exceed N.**Cache TTL is inferred when a transcript omits it.** Older records carry only a flat cache-creation total with no 1h/5m split. Measured across 71,903 records that*do*carry the split, the real mix is**95.2% 1h / 4.8% 5m**— so the old assumption of 5m under-priced that component by 57%. It now assumes 1h, the more expensive reading, matching how unknown model rates are handled. The assumed volume is counted separately and reported, so the adjustment is never silent.\n\nThe cost pipeline is cross-checked against an independent `jq`\n\nimplementation over\nthe same transcripts. Do the same before trusting any number here that matters to\nyou.\n\n**That cross-check once agreed with a number that was twice too high**, and the\nreason is worth stating plainly: the `jq`\n\nimplementation summed usage across every\nrecord, which is exactly the mistake the Python was making. Two implementations\nsharing an assumption agree with each other and are both wrong. An independent\ncheck is only independent where the assumptions differ, so a useful one here has\nto deduplicate on `message.id`\n\n— which the tool now does, and reports:\n\n```\n# what the tool says\nactualis --json | jq '.cost_usd, .duplicate_usage_records_skipped'\n\n# count distinct messages yourself, independently of this tool\ncat ~/.claude/projects/*/*.jsonl \\\n  | jq -r 'select(.message.usage) | .message.id' | sort -u | wc -l\ncd tray-go && go build -ldflags \"-s -w\" -o actualis-tray . && ./actualis-tray\n```\n\nA **constant gauge mark with a status dot in the corner** — the pattern Docker,\n1Password and Teams use, so the app stays recognisable and only the badge\nchanges. Green check when clean, amber when there is something to rotate, red\nwhen it is critical. A newly exposed credential also raises a native\nnotification and flashes the badge.\n\nmacOS, Linux and Windows from one Go codebase, ~2 MB, no Electron and no\nwebview. It is a thin shell over `--json`\n\n; all measurement stays in the CLI.\nSee [tray-go/README.md](/digital-foundry/actualis/blob/main/tray-go/README.md).\n\n`--watch`\n\ntails the transcripts and raises a native notification when an agent\nruns a command carrying a new credential. To keep it running without a terminal,\ngenerate a unit for your service manager. The paths are resolved on the machine\nthat will run it, so there is nothing to substitute:\n\n```\n# macOS\nactualis --service launchd > ~/Library/LaunchAgents/app.actualis.watch.plist &&\n  launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/app.actualis.watch.plist\n\n# Linux\nactualis --service systemd > ~/.config/systemd/user/actualis-watch.service &&\n  systemctl --user daemon-reload &&\n  systemctl --user enable --now actualis-watch\n```\n\nUninstall is one command too:\n\n```\nlaunchctl bootout gui/$(id -u)/app.actualis.watch &&\n  rm ~/Library/LaunchAgents/app.actualis.watch.plist\n\nsystemctl --user disable --now actualis-watch &&\n  rm ~/.config/systemd/user/actualis-watch.service && systemctl --user daemon-reload\n```\n\nThe unit goes to stdout and the install, uninstall and log commands go to\nstderr — so redirecting to a file gives you a working file and still prints\nwhat to do with it. Run `actualis --service launchd`\n\nwith no redirect to read\nthem.\n\nBoth units set `PYTHONUNBUFFERED=1`\n\n. Under a service manager stdout is a file\nor a pipe rather than a terminal, so Python block-buffers it, and without this\nan alert about a leaked credential can sit unwritten for hours.\n\nLogs: on Linux they go to the journal and rotate with it\n(`journalctl --user -u actualis-watch -f`\n\n). macOS has no journal, so output\ngoes to `~/Library/Logs/actualis-watch.log`\n\n; only events are written, never the\nheartbeat, so it grows slowly. Rotation there is opt-in and needs root:\n\n```\nactualis --service newsyslog | sudo tee /etc/newsyslog.d/actualis.conf\n```\n\nlaunchd holds the log file open, so after a rotation the agent keeps writing to the old file until it restarts. That is a property of launchd, not a bug here, and the generated config says so rather than leaving you to discover that logging quietly stopped. Kick the agent to pick up the new file:\n\n```\nlaunchctl kickstart -k gui/$(id -u)/app.actualis.watch\n```\n\nIt is a LaunchAgent rather than a LaunchDaemon on purpose: it must run inside\nyour logged-in session for notifications to post at all, and it should hold\nexactly your permissions and no more. The systemd unit is a **user** unit for\nthe same reason, and declares `ProtectSystem=strict`\n\nand `ProtectHome=read-only`\n\nso the service manager enforces the read-only guarantee too.\n\nIf notifications do not appear, allow them for **Script Editor** in\nSystem Settings → Notifications. `osascript`\n\nposts under that identity.\n\nMeasure, don't interfere.·Read what's already there.Tell the truth, including limits.·Evidence over opinion.\n\n*Local. Read-only. Honest about limits.* Those four lines decide every design\nargument in this repo. `--self-check`\n\nexists because of the third one.\n\nAGPL-3.0-or-later. Copyright (C) 2026 Digital Foundry Solutions, LLC.\n\n**Running this tool places no obligation on you.** Use it privately, inside a\ncompany, on client work, however you like. Running is not distributing, and the\ncopyleft never touches your code, your projects, or your data — none of which\nthis tool transmits anywhere in the first place.\n\nTwo situations do carry an obligation, and both are deliberate:\n\n**Distributing a modified version** means shipping its source under the same licence.**Running a modified version as a network service** means offering that source to its users (AGPL section 13). This is the clause GPL-3.0 lacks, and the reason for choosing AGPL: the plausible future product here is a multi-machine server, and AGPL is what stops someone taking this, closing it, and hosting it.\n\nCopyright is held by a single entity, so a commercial licence for anyone who cannot accept those terms remains available without a contributor agreement.", "url": "https://wpnews.pro/news/actualis-read-what-your-coding-agent-did-on-your-machine", "canonical_source": "https://github.com/digital-foundry/actualis", "published_at": "2026-08-26 17:46:47+00:00", "updated_at": "2026-08-26 18:13:32.735988+00:00", "lang": "en", "topics": ["developer-tools", "ai-agents", "ai-tools", "ai-safety"], "entities": ["Actualis", "Digital Foundry", "Claude Code", "Codex", "Python"], "alternates": {"html": "https://wpnews.pro/news/actualis-read-what-your-coding-agent-did-on-your-machine", "markdown": "https://wpnews.pro/news/actualis-read-what-your-coding-agent-did-on-your-machine.md", "text": "https://wpnews.pro/news/actualis-read-what-your-coding-agent-did-on-your-machine.txt", "jsonld": "https://wpnews.pro/news/actualis-read-what-your-coding-agent-did-on-your-machine.jsonld"}}