Repository navigation
Releases: digital-foundry/actualis
Release list
Actualis 0.6.0
Added
- A PII gate for this repository (
tools/pii-gate.py,make pii, a CI step): it fails when tracked files
carry home paths with real user names, emails, phone numbers, tailnet hosts, public IPs, UUIDs that do not
look hand-typed, or credentials actualis recognises. It decodes and canonicalises before matching, and
reportspath:line: detector, never the value. See CONTRIBUTING.md.
Changed
- Design specs and implementation plans no longer live in this repository.
Actualis 0.5.1
Changed
- The GitHub Action's
descriptionis now under GitHub Marketplace's 125-character limit, so a release can be
listed there. The README shows auses: digital-foundry/actualis@...example.
Actualis 0.5.0
Added
actualis-reputation, an optional companion command and the only part of actualis that uses the network.
updatedownloads Google Safe Browsing threat lists;confirmchecks local matches with 4-byte hash
prefixes mixed with decoys drawn from the downloaded lists;virustotallooks hostnames up on VirusTotal
(opt in; sends hostnames). Every run says what it will send, asks first (--yesin CI), shows ⊕🌐 while
a request is in flight, and records what it did (--receipt,network-log.jsonl). Keys come from
ACTUALIS_GSB_KEY/ACTUALIS_VT_KEYor--key-command, never an argument; the Safe Browsing key travels
in a header, never a URL. Private and local hosts are never looked up or sent. See docs/reputation.md.actualis --reputation: matches the network inventory against the local reputation cache, offline.
New findingsreputation-listed(high),reputation-candidate,reputation-suspicious,
reputation-stale(medium).--reputation --fail-onwith no usable lists exits 2.--json:network.items[].reputationandnetwork.reputation(additive; schema_version stays 1).--self-checkreports when actualis-reputation last used the network.--explain reputation.- Tray: a network badge while actualis-reputation runs.
- Tray: when the Safe Browsing lists are more than a day old, the icon shows the amber update badge and the menu offers "Update Safe Browsing Lists (N days old)…". It opens a terminal running
actualis-reputation update, where you see what is sent and confirm. The tray itself never downloads anything or holds a key. Credential warnings still take priority over the badge (#69).
Changed
- The README says "actualis never uses the network. The optional
actualis-reputationcommand does, only
when you run it, and tells you what it sends."
Actualis 0.4.0
Upgrading from 0.3.0: what can change a gate
The same transcripts can give a different exit code than they did on 0.3.0.
Each of these is listed in full below.
- New agents are read by default.
--agentdefaults toall, which now
includes Gemini CLI and Qwen Code. On a machine with their transcripts, their
commands, credentials and downloads join the report and the gate. --fail-oncan newly exit 3 on transcripts 0.3.0 passed:- new
highremote-execshapes: a shell fed code through a heredoc, a
here-string,cat <<EOF | sh,bash < <(…)oreval "$(curl …)";
curl … | /usr/bin/env bashand other shells named by path; a brace-expanded
program word (cu{r,}l … | sh); and commands that comments, quoting,
heredocs, line continuations or$'…'used to hide from the audit; - newly counted credentials (
high, orcriticalfor a private key): classic
sk-OpenAI keys,npm_,shpat_,lin_api_andya29.tokens, PEM and
armored PGP private keys, quoted JSON keys,aws configure set,gh secret set --body, quoted passwords holding;or spaces, credential options given
with a space (--token VALUE), cookies,.netrclines andAuthorization:
values with no scheme; - newly flagged shell commands:
rm -r -f,rm --recursive --force,rm -rfv,
sudoafterthen,do,(,$(, an assignment ortime,/usr/bin/sudo,
catof~/.ssh/id_ed25519orid_ecdsa,curl --json @file, and a curl
upload to a remote host that mentions "localhost" only in a header or query; - a failing gate whose output was piped into something that exits early
(| head,| true,2>&1 | head -1) used to exit 0 or 120. It now keeps 3.
- new
--iocand--network-strictsee more. Downloads hidden by shell syntax,
options, wrappers, proxies, brace expansion, percent-encoded or numeric hosts,
or padding before an install are now inventoried and matched (network-ioc,
high). A download that cannot be read or resolved (nested too deep, too long,
a host that cannot be normalised, a redirected request, a brace expansion over
16 words) is nownetwork-ioc-unresolvedinstead of passing clean. Under
--network-strict, redirected downloads, a registry given as a variable,
gh/brew tap/npm i owner/repohosts and trusted path prefixes followed by
a shell variable are no longer trusted, sonetwork-unaskedfindings can rise.- Combinations that used to be ignored now exit 2. One mode per run;
--fail-on,--json,--network,--ioc,--ci-log,--with-prompts,
--top,--no-redact,--reason,--quiet,--intervaland the scope flags
are refused where they would be dropped (full list under Changed). A CI line
that passed one of them can now exit 2. - Bounds now exit 2:
--daysbelow 1 or above 36500,--topbelow 1,
--intervaloutside 0.5 to 3600 seconds (ornan/inf), an unknown
--explaintopic, a malformed--suppressid or unknown--whyid (was 1), a
malformed or wrong-schema--diffbaseline (was a traceback or 1), and an
--mcp --rootthat does not exist. - A failed
--self-checkexits 4, not 3. - AF013 (rate table age) no longer fails
--fail-on. A gate that went red
only on the table's age now passes. AF013 is still reported. - Figures move. Resumed or forked sessions no longer count permission modes,
denials and refusals twice; Codex turns are no longer counted twice in AF003
and AISVS 9.2.1; Codex approval modes respect--projectand--days; Codex
under--dayscounts only the window's tokens; dated and alias model ids are
priced as themselves; Google and Qwen models with no published price are no
longer billed at the Claude ceiling. AF003, AF006, AISVS 9.2.x and the
spend-based findings (AF001, AF007, AF009, AF010) can appear or vanish. - Counts change (
secret_exposures,secret_projects,
unreadable_commands,duplicate_tool_calls_skipped), andreport_sha256is
not comparable with 0.3.0. Take a fresh--diffbaseline. --fail-on anycan newly exit 3 on the new mediumnetwork-configflag:
a command that writes download configuration (gitinsteadOfor proxy,
npm/pnpm/yarn registry or proxy, pip index, or a write to.npmrc,
.yarnrc,pip.conf,.gitconfig).- Cookie suppressions need re-adding. A cookie credential's id now names
the cookie (cookie:<program>:<NAME>), not the header, so a suppression
written for a cookie finding on 0.3.0 no longer matches. - GitHub Action:
fail-onis case-insensitive,fail-on: Noneno longer
skips the gate silently, and an unrecognised value fails the job.
Added
-
Gemini CLI support. Reads
~/.gemini/tmp(or
$GEMINI_CLI_HOME/.gemini/tmp, plus~/.cache/.gemini/tmpfrom macOS sandbox
runs): the legacy.jsonsessions, the JSONL sessions with$setand
$rewindTo, and subagent sessions.- Shell commands are audited and inventoried like every other agent's.
web_fetchURLs (taken from its prompt) andgoogle_web_searchreach the
network inventory;write_fileandreplacereach the audit-config
tripwire. - Gemini CLI records no approvals, so every Gemini network item's approval is
unknown. A call a person declined, or a policy blocked, counts as a
refusal. - Tool calls a later rewind removed are still audited, marked
rewound, and
counted inrewound_tool_calls.
- Shell commands are audited and inventoried like every other agent's.
-
Qwen Code support. Reads
~/.qwen/projects(or$QWEN_HOME,
$QWEN_RUNTIME_DIR), with the real cwd and branch per record and the approval
mode per turn:yoloandautocount as unasked;auto-edit(which still
asks before shell commands),defaultandplanas unknown. -
--agent geminiand--agent qwen. Both agents also work with--root,
--watch,--replay,--self-checkand the MCP server.--explain gemini
and--explain qwendescribe them. -
Gemini API prices from Google's price list, including the long-prompt rate
above 200,000 tokens for Pro models and the announced price changes for
gemini-3.6-flash, gemini-3.8-flash and gemini-4-argon, applied by message
date. -
An
unpricedpricing tier. Models with no published price
(gemini-3-pro-preview, gemini-3.5-flash, every Qwen model) show their tokens
and no cost, instead of a guessed one. -
make securitybuilds a pinned Kali image and runs bandit, trufflehog,
gitleaks and a transcript fuzzer (Claude Code, Codex and Copilot CLI formats)
against a read-only checkout, with no network and a throwaway home. It works
on native Linux Docker hosts, reports a scanner that failed to run as FAILED,
and reports unverified secret candidates outside the fixture allowlist.
Documented in CONTRIBUTING.md and SECURITY.md. -
make security-remoteruns the same scan on the build server over SSH
(SECURITY_HOST, defaultmac-mini). It sends a git bundle ofHEAD
(committed work only) and copiessecurity-out/back. -
New
--jsonfields. All are additive, soschema_versionstays 1:scope(days,project,agent) andcoach[].project;unsupervised_pct,moded_commandsandunmoded_commands;bash.flagged.highandbash.flagged.medium(suppressed flags left out);unparsable_lines,unsupported_records,rewound_tool_callsand
unpriced_model_tokens;vendors.capabilities[].geminiand.qwen, andnetwork.items[].rewound;- two new
network.ioc.matches[].reasonvalues,host-unresolvedand
redirected; - in
--replay --json,window.undated_sightings.
-
MCP additions.
fleet_summaryaddsmoded_commands,unmoded_commands,
unsupervised_basisandunpriced_model_tokens;exposed_secretsrows carry
suppressedandsuppressed_reason;coach_findingsincludesproject. -
GitHub Action
gate-reasonsoutput, counting what the gate actually failed
on, including flagged shell commands and IOC hits. -
--diffwarns when the two reports were made with different filters or
roots. -
network-configshell-audit finding (medium), so a download redirect
set in one tool call is visible even when a later call's download cannot be
tied to it.
Changed
- One mode per run. Combining two of
--diff,--why,--replay,
--watch,--mcp,--explain,--agents,--suppress,--suppressions,
--self-check,--service,--completions,--card,--share,--coach,
--aisvsand--bashis a usage error that names both. Before, one was
silently dropped, including a--suppresswrite. - Flags a mode would drop are refused (exit 2), naming both flags:
--fail-onwith a mode that never evaluates the gate (--diff,--why,
--replay,--watch,--mcp,--explain,--agents,--suppress,
--suppressions,--self-check,--service,--completions);--jsonwith--share,--coach,--aisvs,--bash,--why,
--explain,--suppress,--suppressions,--self-check,--service,
--completions,--watchor--mcp;--networkand--iocwith modes that would drop them;--iocwith
--why;--ioc,--network-strictor--network-trustwith--coach,
--aisvsor--shareunless--fail-onis given;--ci-logwith--root,--agent,--project,--replayor a mode that
does not scan;--with-promptswith--agents,--replay,--watchor--mcp;--topwith--json,--htmlor any mode other than the text report,
--bashand--network;--reasonwithout--suppress;--quietor--intervalwithout--watch
(or with--service newsyslog);--root,--agent,--days,--projector--no-redactwith
--explain,--agents,--completions,--suppressor--suppressions;
--daysor--projectwith--watchor--service;--projector
--no-redactwith `...
Actualis 0.3.0
Upgrading from 0.2.2: what can change a gate
The same transcripts can give a different exit code than they did on 0.2.2.
Each of these is listed in full below.
--fail-on highcan newly exit 3 on transcripts 0.2.2 passed:- new
highremote-execshapes (bash <(curl …),source <(…),
sh -c "$(curl …)", dequotedcu''rl … | s''h,curl … |& sh, and a
shell at any later stage of the pipeline); - newly counted credentials (passwords passed as options, short
PASSWORDvariables, short URL and scp passwords, long password-less
userinfo, unprefixedAuthorizationheaders), which arehigh; - the new
audit-configfinding, which ishighand cannot be suppressed.
- new
--fail-on anycan newly exit 3 on the newmediumoversized-command
flag.- Coach findings move with cost. The new pricing changes spend figures,
and spend-based findings (AF001, AF007, AF009, AF010) can appear or vanish. - Only when you opt in:
--network-strictaddsnetwork-unaskedfindings
(any);--iocaddsnetwork-ioc(high) andnetwork-ioc-unresolved
(any). - Counts rise (
secret_exposures,secret_projects,
unreadable_commands), andreport_sha256is not comparable with 0.2.2. Take
a fresh--diffbaseline.
Changed
-
Faster. The shell-audit rules and unreadable shapes check a hand-written
literal prefilter before each regex (results are identical; a test compares
both paths over every test string and 5,000 generated commands), and
--networkwithout--fail-onskips the audit and secret ranking it never
shows. On a 1,169-file corpus--networkwent from 43 s to about 6 s and a
full run from 44 s to about 21 s. -
New
highremote-exec detections. Besidescurl … | sh, the shell
audit now flagsbash <(curl …),source <(…)and. <(…),
sh -c "$(curl …)"(any shell, any-ccluster such as-lc), a dequoted
cu''rl … | s''horc\url … | busybox sh,curl … |& sh, and a shell at
any later stage of the pipeline (curl … | tee f | sh). Each command is
flagged once. These can newly fail--fail-on high(exit 3) on transcripts
0.2.2 passed. -
Location-based ids for person-chosen passwords. A password passed as an
option (curl -u user:PASS,--user=,-U,wget --password=,docker login -p), and every credential first counted in this release (see Fixed),
gets an id derived from where it appears (sha256("opt:" + program + ":" + option + ":" + user)[:8]and the like), not a hash of the value, so a
published id cannot confirm a guessed password. Credentials counted in 0.2.2
keep their value ids. See docs/secrets.md. -
A suppression that covers more than one value is ignored. When a
suppressed location id has seen more than one distinct value in the run, the
secret is treated as unsuppressed, for display and for--fail-on, with the
reason "suppression covers one value; N seen". -
-u user:pwmasking fails closed. The password after-u/--useris
masked for every program except a short list where-unames a user, not a
password (dockerandpodmanexec/run/create,docker compose exec,
sudo,su,ssh,chown,systemctl,gitand similar), and the program
that receives-udecides, past wrappers such assudo,envandtimeout.
A numericuid:gidis never a credential. -
report_sha256no longer moves with the clock. The digest excludes
pricing.age_days,pricing.staleand theAF013coach finding, which change
as days pass over unchanged transcripts.report_sha256values are not
comparable with 0.2.2 reports. This is a known break of digest comparability,
not of schema;schema_versionstays 1.--diffsays so when the digests
differ but nothing it compares has changed. -
Current-generation pricing. Opus 5.5 ($4/$20), Sonnet 5.5 ($2/$10),
Haiku 5.5 ($0.10/$0.50, or $0.50/$2.50 for a message whose prompt exceeds
100,000 tokens), Fable 5.1 and Mythos 5.1 ($10/$50) are vendor rates,
verified 2026-10-08. The cache-read multiplier is now per model: 0.10 by
default, 0.05 for Opus 5.5 and Sonnet 5.5, 0.025 for Fable 5.1 and Mythos 5.1.
It applies to Claude Code, the subagent cost floor, Copilot and the cache
savings figure. An unknown model in a known family is now priced from the
newest known sibling, not the most expensive one. Fast mode, batch and data
residency are not modelled.
Added
--network --htmlwrites the network report as one offline page:
inline SVG charts, sortable tables, every item with its trace, and the
--network --jsonobject embedded with its sha256. It never overwrites
(actualis-network-2.html, …), is readable by you only, and a content
security policy blocks every fetch.--outnow applies to--cardor
--html, and--with-promptsto--jsonor--html.--networkprints only the NETWORK section (with the IOC block when
--iocis given), and--network --jsonemits only thenetworkobject. The
full report gets a one-linenetworksummary in FLEET when downloads exist.
The view reads top down, every line at most 100 columns:- a chart strip: an approval bar (unasked solid, unknown medium, asked
light), downloads per day as a sparkline with the unasked on the same
scale under it (empty days blank), and the mix of kinds; - TOP HOSTS, TOP PACKAGES and, with two or more projects, BY PROJECT: up to
ten rows each, a bar whose solid part is the unasked share, and
unasked / all; - UNASKED, BY SESSION (UNKNOWN when nothing is unasked): the newest five
sessions, each with its first five such downloads in the order they ran,
the agent'swhy, and theactualis --network --session <id>command
that lists them all.
The charts need no colour, and an ASCII-only terminal gets ASCII (#,.,
:and_.:-=+*).--explain networknames the--jsonpath behind each
chart.
- a chart strip: an approval bar (unasked solid, unknown medium, asked
--host,--package,--sessionand--unaskednarrow--network
(text and--json), combinable with each other,--projectand--days.
--hostis a suffix on a label boundary,--packagethe exact name (npm
case-sensitive, pypi by PEP 503),--sessiona prefix. The filter runs
before--network-strictand--ioc, so totals, findings and matches are of
the filtered set. Filtered, the text view prints the chart strip for the
filtered set, then a block per item under a line naming its session, newest
first and capped at--topx 5: host and target, when, agent, project and
session, approval with the mode it was judged from, the redacted command,
why, the prompt, and atraceline (call id, file and line) that
sed -n '<line>p' <file>turns into the exact record. Without--network
each is a usage error.network.by_day[],network.by_day_undated,network.by_project[]and
network.packages[].unasked, the series the charts draw. They reconcile
withnetwork.totals(dated plus undated equals all items; project sums
equal the item and unasked totals), and the text view says so in red when
they do not.network.items[]gainsmode,why,promptandtranscript.mode
is the permission-mode key approval was judged from.whyis the agent's
own text before the call (last sentence, 160 characters);promptis the
person's last real message before the turn (160 characters), emitted only
with the new--with-promptsandnullotherwise. Both are redacted and
cleaned when read.transcriptis{root, file, line}. What Claude Code,
Codex and Copilot each provide is inactualis --explain network.
schema_versionstays 1;--shareand--cardcarry none of it.secrets[].distinct_valuesin--jsonand in the MCPexposed_secrets
tool: how many different values were seen under the id in this run. The
text report says "N distinct values" when it is more than 1.- More unreadable shapes are counted in
unreadable_commands: ANSI-C
quoting that spells a letter ($'\x63url'), a substitution piped to a shell,
and an inline script that reaches the network (python -cwithurllib,
requestsorsocket,node -ewithfetch,perl -MLWP). They are
counted, not flagged. bash.oversized_commandscounts commands over 32 KB, and each raises a
mediumoversized-commandflag (see Fixed).--ioc FILE: did an agent install the bad version? Matches every
download the transcripts show in the network inventory against a known-bad
list, offline and only
when named: theactualis-iocline format (npm:x@=1.2.3||=1.2.4,
pypi:name,host:evil.example) or OSV JSON/JSONL. Names compare as each
ecosystem does (npm and Go exactly, pypi by PEP 503, images through the shared
Docker Hub normaliser); versions by SemVer 2.0.0 or PEP 440, written out in
the standard library. A match is a high finding (network-ioc); a listed name
whose version cannot be decided is medium (network-ioc-unresolved), never
clean. Refused calls are listed and never fail a gate, and the trust list
never exempts a match.network.iocin--json(always an object),
network.items[].ioc, an IOC block in NETWORK,--explain iocand
docs/ioc.md. Any problem loading a list, or a list with
nothing checkable, exits 2; every size, line, entry and nesting limit is hard.
The Action gains aniocinput and anioc-matchesoutput.- Network inventory. A NETWORK section and a
networkkey in--jsonlist
every download and fetch the transcripts show and actualis can read —
packages installed, repos cloned, URLs fetched, including those behind
control flow, wrappers (sudo,timeout,xargs,eval),bash -cand
substitutions — and whether a person approved each (asked,unasked, or
unknownin default mode, where an allowlist rule may have allowed i...
Actualis 0.2.2
Fixed
Found by an audit of every calculation for explainability, traceability and
determinism.
- Results no longer depend on the order the filesystem lists transcripts.
When the same message appears in two transcripts it is counted once, and the
copy kept decides its day, branch and ticket. Files were read in directory
order, which differs between machines, so a Mac and a Linux runner could
report different per-day and per-branch figures and a different
report_sha256for the same files. Files are now read in sorted path order. - AF012 no longer fires a false critical on Codex- or Copilot-only fleets.
It counted their sessions toward its 500-message threshold, but sessions are
never re-emitted, so--fail-on criticalfailed on 500 Codex sessions. It
now counts Claude Code messages only. refusals.by_weeklabels weeks with their ISO year. 2025-12-29 was
labelled2025-W01instead of2026-W01, colliding with January.- The MCP server's
dayswindow matches--days. It counted back from
the current time instead of from midnight UTC, so the samedayscovered an
extra partial day and reported a different cost than the CLI. - Models priced from an aggregator or vendor docs are no longer reported as
unpriced. They were added tounknown_modelsand
cost_usd_from_unpriced_models, and the report said they were billed at
Opus-tier rates.aggregator_priced_modelsis now filled for Claude Code,
Codex and Copilot.
Actualis 0.2.1
Fixed
- The GitHub Action could not load in 0.2.0. GitHub evaluates
${{ ... }}
expressions anywhere in arun:block, shell comments included, and the
comment explaining 0.2.0's own fix contained an empty one. Every caller got
"An expression was expected". The CLI was not affected. A new test fails the
build on any empty or unclosed expression inaction.ymlor a workflow, so
this is now caught before a release instead of by the post-release
self-test.
Actualis 0.2.0
Actualis now reads Copilot CLI. Post your card.
Added
- GitHub Copilot CLI sessions are read, from
$COPILOT_HOME/session-state/*/events.jsonl(default~/.copilot): shell
commands into the same audit, cost per model per session, supervision per
command, refusals, subagents and premium requests.--agent copilotreads it
alone. Copilot reportsinputTokensincluding cache reads and writes for
every provider, and Claude model ids with dots; both are handled, and
--explain copilotsays how. A person declining a prompt is recorded as
denied-interactively-by-user, captured from a real denial, and is reported
as a human refusal. --cardwrites a 1200×630 SVG and PNG to post:supervision,costor
volume, in aheroorterminalstyle. Standard library only. The SVG and
PNG are drawn from one list of pixel runs, so they cannot disagree. Nothing
identifying can reach either, and the leak test now checks both, plus the
printed caption.--cardrefuses to combine with another mode ----json,--fail-on,
--diff,--why,--share,--watch,--mcp,--replay,--self-check,
--explain,--agents,--suppress,--suppressions,--completionsor
--service-- so a card run can never silently skip a CI gate or quietly
write nothing.- The MCP server (
--mcp) now sees Copilot sessions too. - Copilot sessions with no shutdown record are reported, not dropped. The
report saysN Copilot sessions unpriced (no shutdown record)under the cost
line, and--jsoncarriescopilot_unpriced_sessions. No cost is estimated
for them.
Changed
- The card counts supervision per shell command, using the permission mode
in force when each command ran, rather than per turn.--sharestill counts
per turn, but--shareand the card now share the one definition of
"unsupervised" the AISVS mapping already used, socodex:nevercounts there
too. - MCP
fleet_summaryuses the same single "unsupervised" definition, so
codex:nevercounts as unsupervised there as well. - The vendor matrix (
--explain vendors,--json) has acopilotcolumn.
Fixed
--shareprinted raw model ids. A fine-tune or private deployment id
could reach a summary documented as containing nothing identifying. Model
names now appear only when they are exact entries in the public price table;
anything else iscustom. The leak test seeds a private model id.- A GitHub expression inside the Action's
run:block killed the audit
step. GitHub expands${{ ... }}before bash sees the script, so a help
message quoting one left a dangling quote and a syntax error on the runner.
Actualis 0.1.14
Fixed
-
The GitHub Action reported nothing back exactly when it had something to
say. A composite action that exits non-zero does not propagate its outputs,
soexit-codeandfindingscame back empty wheneverfail-onfired —
the one case a caller most needs them. The gate worked; the reporting did
not.The audit step now always exits 0 and a separate
Gatestep does the
failing, so the outputs come from a step that succeeded. The self-test now
asserts both halves of the contract: that the job fails, and that the
outputs survive.Found by the self-test on the first run against a published release, which is
what it was written for. It had never run automatically, because of the
second bug below. -
The action self-test could never trigger. It listened for
release: published, but a release created by a workflow using the default
GITHUB_TOKENdoes not trigger other workflows — GitHub blocks that to
prevent recursion. It now runs onworkflow_runwhen the release workflow
completes, and skips when that release failed, since a failed release
publishes nothing to test.
Actualis 0.1.13
Added
-
--ci-log FILEaudits an agent that ran inside CI, and a GitHub Action
wraps it. Until now the tool audited a developer's laptop. Agents
increasingly run in CI, where the record lands on org infrastructure by
construction — which is the population this was always aimed at and could not
reach.On a runner there is no transcript directory to discover: the agent ran in
this job. The Claude Code Action
drives Claude Code through the SDK and writes every event to a single JSON
array at$RUNNER_TEMP/claude-execution-output.json, exposing the path as
itsexecution_fileoutput.--ci-logreads that.Reading the documented output rather than guessing at
~/.claudeon the
runner is the deliberate part. Whether a transcript directory exists there
at all is an SDK internal that could change without notice;execution_file
is a published contract.The records inside are the ones the local parser already understands — the
action's own README describes them as "top-level events withtype;
assistant text is nested undermessage.content". So the record-level
ingest is now shared:_ingest_claudeis the only place that knows what a
record means, and a change to that meaning cannot apply to one
serialisation and not the other. There is a test asserting the two agree on
message count, dedup, cost, commands, permission modes and credentials.action.ymlat the repository root wraps it: pass the upstream step's
execution_file, get a gate, a job summary and an optional JSON report.
Credentials are redacted, and the self-test fails the build if a raw
credential ever reaches an artifact the action produces.What this does not cover: an execution log is one job's events. Cost, the
shell audit and credential detection all work; there is no history before the
run, so--diffagainst a saved baseline is how to ask what changed, and
blast-radius replay is bounded by the single job.