Repository navigation
Releases: kodji7202-code/runledger
Release list
RunLedger 0.7.0
[0.7.0] - 2026-10-20
Added
- Selective restore with
runledger rollback <id> --path P(runledger.rollback): the option
(repeatable) restores only the named file or folder and leaves everything else exactly as it is.
A value is read against--cwdor may be an absolute path inside the repository; matching is
literal, sosrcrestoressrc/x.txtbut neversrc2/y.txt, and names with[or*behave.
Directories restore their whole subtree, including the deletion of files created after the
checkpoint (labelleddelete, not in checkpoint); a value that is already identical prints
Nothing to restore for <path> (already matches <id>).and exits 0. Paths outside the
repository,.gitand its contents, empty values and values present in neither the checkpoint
nor the working tree are refused with a one-line error before anything is written. All the
whole-tree safety stays in place: one safety checkpoint of the whole tree is still created
first (sorunledger rollback <safety-id>undoes a partial restore), the stale-preview check
still covers the whole tree, and the symlink, junction,.gitoverlap, reserved-name and
case-collision refusals apply to an in-scope path exactly as before.--pathcombines with a
checkpoint id,--run,--last,--dry-runand--yes, and the preview lists the scope, each
change, and a summary with the number of files and the bytes to restore.--path .is the same
as a whole-tree rollback, which is unchanged without the option. - Droid discovery follows
FACTORY_HOME_OVERRIDE(runledger.adapters.droid): the variable
is read as a HOME, so the Factory config directory becomes<FACTORY_HOME_OVERRIDE>/.factory,
and that is where sessions, settings files and a global hook file are looked for.
FACTORY_HOME, which names the config directory itself, still wins over it; without either,
the directory is~/.factory. - MCP calls are graded by their verb (
runledger.mcp_risk, new; used byrunledger.risk): a
destructive tool (delete_project,drop_table,reset_branch,purge_bucket) is a high
mcpfinding with the reasonCalled a destructive MCP tool (<server>: <tool>), a
state-changing tool (create_post,send_message,deploy) is medium, and a read-only or
unknown name keeps the previous low finding and its wording. Words are matched exactly and case
insensitively, sodeleted_itemsandundelete_itemsstay low while camelCase and kebab names
(deleteBranch,delete-branch) split into words; a leaf whose first word is a read verb
(get_delete_policy) never escalates. Both name shapes agents report are understood:
mcp__<server>__<tool>(Claude Code, Codex) and the bare connector name
(supabase__delete_project, Factory Droid). - The SQL argument of an execute-class MCP tool is inspected (
runledger.mcp_risk, new): a
call whose name carriessql,query,statementorexecute, or that carries a
query/sql/statementargument that opens like a statement, is graded by that statement
instead of its verb:DROP,TRUNCATE, aDELETEorUPDATEwithout aWHEREand
ALTER TABLE ... DROPare high, any other data-changing statement is medium, read-only SQL
keeps the low finding, and a call with no SQL the guard can read is medium. Strings and comments
are blanked before matching, soselect 'DROP TABLE x'stays low. The SQL body is never copied
into a reason,guard.log, an approval request or a receipt, and an argument of up to 2 MB is
capped at 20 000 characters before any pattern runs. - Twenty-two more dangerous-command rules (
runledger.risk): force-deleting a git branch
(git branch -D,git branch --delete --force), discarding all working-tree changes
(git checkout .,git restore .),git stash drop/clear,git push --deleteand
git push origin :branch(high),find ... -deleteandfind ... -exec rm|unlink|shred,
docker system|builder|image|container|network prune,gh <resource> delete,aws s3 rmof a
single object,rsync --delete,dd of=<file>andreg deleteof a user key (medium),
helm uninstall,gh repo delete,docker volume pruneanddocker system prune --volumes,
aws s3 rm --recursive,aws s3 rb --force,dd of=/dev/...,rd /s /q, a shell
DELETE FROMwithout aWHEREandreg deleteof a machine-wide key (high), plus PowerShell
Remove-Item -Recursewithout-Force(medium; the existing-Forcerule stays high and does
not also report it). Every rule is confined to one command (the lookahead stops at;,&,|
and newlines) and runs on the quote-, comment- and heredoc-blanked view, so a commit message or a
string that mentionsgit branch -Dis not flagged. - Rules you write yourself (
command_rules,tool_rules,mcp_verbs): the user file
(~/.runledger/config.json) and a project's.runledger.jsonmay now carry three top-level
keys.command_rules.deny/.asktake regular expressions, or objects withpattern, an
optionalreasonandraw; they are searched case-insensitively against the same
quote- and heredoc-blanked view the built-in command rules use, andraw: truereads the whole
command instead.tool_rules.deny/.asktakefnmatchglobs matched against the whole tool
name, case-sensitively, for any tool (Edit,mcp__vercel__*); a tool on both lists is denied.
mcp_verbs.destructive/.write/.readadd words to the MCP verb vocabularies, so a server
whose destructive tool is named something else is graded as expected. Hits arerule_deny
(high) andrule_ask(medium) findings whose reason names the entry. A rule hit is always
decided: replacing the default deny list keeps it, and no!exclusion can drop it. Both files
only add, so a project can tighten with rules but never remove one of yours; a project
mcp_verbs.readlist, a project exclusion aimed at a rule hit and a pattern that is not a safe
regular expression are ignored and logged as warnings. Receipts: a receipt re-runs the rules with
the project's file only, so a rule that lives in your user file is enforced live but does not
appear in a receipt built from an older session (docs/guard.md). - The guard reads Claude Code, Factory Droid and Codex payloads (
runledger.guard): the hook
command carries--agent claude-code|droid|codex(without the flag the agent is inferred from the
payload, and Claude Code is the default, so existing hooks are unchanged), so a DroidExecute
is decided as the shell call it is,Create/Editas writes and edits,ApplyPatch/apply_patch
as one step per file the patch touches (one bad file denies the whole call, and the log lists the
paths, never the bodies), its searches asSearchandFetchUrlasWebFetch; MCP names keep
both shapes (mcp__server__tooland Droid's bareserver__tool). The log keeps the agent's own
tool name and addsagentandcanonical_tool(ExecutebecomesBash), andsession_idstays
the agent's own session id, so a receipt can match the decisions to the session. Codex cannot
prompt from a hook, so an ask becomes a deny there with a reason that names the rule and how
to proceed (guard.codex_ask: "allow"in the user file opts out; a project file cannot set it),
while Claude Code and Droid keep asking. PowerShell commands sent underBash(Codex) or
Execute(Droid) are analysed as PowerShell. An unknown--agent, a malformed payload or an
internal error never blocks a call: the hook exits 0, keeps the fail-open/fail-closed semantics
and reports a bad agent on stderr. A hook command stored with--agentis recognised as
RunLedger's own byinstall,uninstalland the install-time self-test,guard test --agent X
reads a native payload, and the hook stays fast and offline (docs/guard.md). - One
guard installfor Claude Code, Droid and Codex (runledger.hook_targets, new;
runledger.guard):runledger guard install|uninstall [--agent claude-code|droid|codex|all] [--project PATH | --global]writes each agent's own file. Claude Code keeps
<project>/.claude/settings.json/~/.claude/settings.json(the unchanged default); Factory
Droid gets<project>/.factory/hooks.json/<FACTORY_HOME>/hooks.json(or
<FACTORY_HOME_OVERRIDE>/.factory), keyed by event with nohookswrapper and never its
settings.json; Codex gets<project>/.codex/hooks.json/<CODEX_HOME>/hooks.json, with the
events wrapped inhooks, matcher.*andconfig.tomlnever touched. The guard command carries
the agent ("<python>" -m runledger guard --agent droid|codex;--portablekeeps the bare form),
the checkpoint hooks stayrunledger checkpoint --hookfor all three, andinstallruns the
stored command once. Installs are idempotent (one guard entry per file,<file>.bakwritten once
and never rewritten), keep every foreign hook and key, refuse a malformed or wrongly shaped file
untouched (exit 1, nothing written, no.bak/.tmp) and accept an empty or BOM-prefixed one;
uninstallremoves only RunLedger's entries plus the groups, events andhooksobject they
emptied, and an agent without a hook file is reported without creating one.--agent allinstalls
in a project for all three and, globally, only for agents whose config folder already exists (it
never creates~/.factoryor~/.codex, and names each skipped agent with the reason).guard test --agent droid|codexprints the decision and that agent's hook output, and the install output
says what each agent still needs: Droid snapshots its hooks at startup (restart or/hooks), and
Codex runs only hooks you trust and turns an ask into a deny (guard.codex_ask) - neither message
claims the hook is verified inside the agent, which only a real session shows
(docs/guard.md,docs/agents.md, `docs/rollb...
RunLedger 0.6.1
[0.6.1] - 2026-10-18
Changed
-
A Droid run with no recorded usage no longer prices its models (
runledger.client,
runledger.server): the push body and the stored run keepcost_usdnullon every model of
such a run instead of the0.0the priced-model bucket used to carry, so a model group made only
of missing-usage runs showscost_usdnullwithpriced_runs0 inGET /api/statsand
GET /api/insights(n/a in the dashboard), not a free$0.00. -
Insights agent aggregates carry their lower-bound flag (
runledger.server):agents[]in
GET /api/insightsnow returnspriced_runs,partial_runsandcost_is_lower_bound, so an agent
whose runs are all only partly priced reads≥ $Xin the dashboard instead of a complete figure. -
The compliance report marks a partly priced developer as a lower bound (
runledger.server): a
developer row inGET /api/export/report.htmlreads≥ $Xwhen any of that developer's runs is
only partly priced, matching the team figure above it (previously only a run with no cost at all
produced a mark). -
The dashboard budget card shows
n/afor an unpriced month and≥ $Xfor a partly priced one
(runledger.server): a month whose runs all have unknown costs no longer reads$0.00 spent, and
a month with partly priced runs is marked≥, in both the team figure and the developer rows. The
note next to the figure names how many runs it leaves out. The numeric spend behind the alert
thresholds and the progress bar is unchanged. -
Quality score: an install word inside a test argument no longer hides the run
(runledger.quality): an install verb (install,uninstall,add,i,remove)
disqualifies a segment only when it is a package manager's subcommand in the command word or
wrapper chain (pip install pytest,python -m pip install pytest,uv pip install,uv add,
poetry add,npm install,npm i,pnpm add,yarn add,bun add). Real runs whose
arguments contain those words earn theirtests_runcredit again, as they did in v0.6.0:
pytest -k add,pytest -k "install and not slow",npm test -- --grep installand
jest -t uninstallare runs. Inspection flags (--version,--help,--collect-only,
--list-testsand the rest) and every other install and inspection case behave as before. -
Text sent to Claude masks e-mail addresses and home folders, with no opt-out
(runledger.llm,runledger.review,runledger.summarize): every request body, from--ai
summaries as well as from--review, replaces e-mail addresses with[EMAIL]and the user name of
C:\Users\<name>,/Users/<name>and/home/<name>with~, on top of the secret patterns that
were already applied. Masking happens before the text is cut to its limit, so a home folder is
never sent in half, and it applies to the whole message at the API boundary. A session mentioning
ana@example.comorC:\Users\ana\app\x.pynow sends[EMAIL]and~\app\x.py; commands keep
their shape. A review's data block also escapes<as\u003c, so a command containing
</run_data>cannot close it.docs/analysis.md,docs/quickstart.mdand the README state what
is masked. -
A home folder whose user name contains spaces or punctuation is masked whole, and nested step
inputs are masked before--aiwrites them as JSON (runledger.llm,runledger.summarize): a
quoted path such as"C:\Users\Jane Smith\app\x.py"or/Users/Jane Smith/app/xwas masked only
up to the first word, leavingSmithin the text sent to Anthropic; the user name is now consumed
to the separator or quote, so both read"~\app\x.py"and~/app/x. Inside quotes the name runs
to the next path separator, so a folder namedJane (Smith)goes whole
("C:\Users\Jane (Smith)\app\x.py"sends"~\app\x.py"), and the same name is masked when it
appears unquoted. A step input is masked at every level before serialization, so aMultiEdit's
edits(and the doubled Windows separators that JSON produces) can no longer carry a complete
home path into an--airequest. The check at the API boundary also recognizes a path whose
separators were doubled, and command boundaries are kept:cat ~/app/x && lsstill sends&& ls. -
The dashboard reads a second, read-only connection, and a backup no longer stops writes
(runledger.server): reports, stats, insights, the run list and the exports read from their own
read-only connection (file:...?mode=ro), so a long report or arunledger backuprunning beside
the server cannot hold the write lock and block pushed runs.runledger backupcopies through that
connection in pages with a short sleep, so it stays cheap on a busy store. Test databases that live
in memory keep their single connection. A read never sees uncommitted rows, which is what the
approval decision flow needs. -
Dashboard: unknown costs show
n/a, partial costs show≥$X, and the unpriced hint names the
real cause (runledger.server): a run with no recorded token usage or no known model price shows
n/a(never$0.00), with a tooltip saying token usage was not recorded when that is the reason,
and a figure that leaves unpriced tokens out is prefixed with≥in the run list, the KPI card and
the by-user, by-project, by-agent, by-model, insights and cost-per-run cells. The hint under the cost
KPI now splits the unpriced runs into those without recorded usage and those with an unknown model
price instead of always blaming the price. -
Dashboard filters: Max quality and AI verdict, kept in the URL (
runledger.server): the filter
bar next to Min quality has a Max quality number input and an AI verdict select
(looks_safe,needs_review,dangerous, or any), and the whole filter set (developer, project,
agent, min risk, min quality, max quality, verdict) is written to the URL hash and restored from it,
so a reload or a shared link shows the same runs. The filter bar belongs to the Runs view: switching
to Insights, Approvals, Keys, Audit or Settings leaves it untouched, and returning to Runs writes
the filters back into the hash, so a tab switch no longer loses what the developer typed (and a
filter edit landing after the switch cannot pull the page back to Runs). -
Dashboard polling pauses while the tab is hidden and backs off after a failure
(runledger.server): the pending-approvals poll on the dashboard and the poll on the individual
approval page now stop while the page is hidden, refresh at once when it becomes visible again, and
grow the delay from 3 s to 6 s, 12 s and so on up to 30 s after a failed request (a 429 included)
instead of hammering a server that is down. The first success returns to the 3 s delay. -
Grade E is gone from the quality distribution (
runledger.server):GET /api/insightsreturns
thequality.by_gradebuckets A, B, C, D and F, the dashboard shows five buckets and no E badge, and
a run stored earlier with grade E keeps its score but shows no grade badge. -
Dashboard comments and the run detail panel match the real page (
runledger.server): the header
comment no longer claims features hide on 404/403 because endpoints may not exist (only admin-only
panels hide, on 403), and a run row opens a detail dialog that names the developer, the key that
pushed the run ("Pushed by alice-laptop"), the cost, risk, quality, AI verdict, steps, tokens and
revision, with a link to the receipt. -
Project
.runledger.jsonis tighten-only in receipts as well as the guard:ignoreand
loweringseverity_overridesare read only from your user config (~/.runledger/config.jsonor
RUNLEDGER_CONFIG). A project.runledger.jsonmay raise severities and add safe
extra_secret_paths; if it tries to ignore or lower risk kinds, the receipt keeps the findings
and adds a note. -
runledger guard installwrites interpreter-bound hook commands by default:guard install
writes"<sys.executable>" -m runledger guardand"<sys.executable>" -m runledger checkpoint --hook
so the hooks run withoutrunledgeronPATHand when the Python path contains spaces, upgrades
earlier barerunledger ...hook entries in place, and verifies the configured command exits0
on empty JSON input, run from the project folder with your environment unchanged (a command that
cannot start RunLedger there is reported as a warning, not as a success). Pass--portableto
write barerunledger ...commands for checked-in settings files. -
HTML receipts inside a Git work tree are safe by default, and default receipt filenames
auto-increment (runledger.cli):runledger receiptnow enables safe mode by default for HTML
files written inside a Git repository (matching Markdown and JSON; pass--no-safefor full
paths), and when-ois omitted it writesrunledger-<stem>-2.<ext>,runledger-<stem>-3.<ext>,
and so on instead of overwriting an existing receipt file. -
The dashboard sign-in form replaces the key-in-URL instructions (
runledger.server): the401
sign-in page now shows a form (method="post", action/session) instead of teaching
/?key=YOUR_API_KEY, and a wrong key answers the same page with an error message. Docs
(docs/api.md,docs/enterprise.md,docs/self-hosting.md,docs/quickstart.md) describe the
form, the Strict cookie, the confirmation step and the session cap; the?key=link is documented
as deprecated. -
The Docker image identifies itself, and the Compose stack is hardened (
Dockerfile,deploy/):
the image carries the full OCI label set, withorg.opencontainers.image.versionand
org.opencontainers.image.revisionfed by theRUNLEDGER_VERSIONandRUNLEDGER_REVISIONbuild
arguments (the CI docker job passes the version frompyproject.tomland the commit it built, then
checks the labels on the ...
RunLedger 0.6.0
Changed
- Safe mode is the default for shareable receipts: Markdown and JSON receipts written to
stdout or inside a Git work tree shorten local paths (repository-relative,~for the home
folder,…for shared orRUNLEDGER_SAFE_PREFIXESprefixes) and mask e-mail addresses and URL
credentials. HTML and files outside a repository keep full paths.--safeand--no-safe
override the default; JSON receipts add asafekey. The GitHub PR comment is always safe.
Added
- JSON receipt contract: JSON receipts carry
schema_version: 1.runledger/receipt.schema.json
ships with the package anddocs/receipt-json.mddescribes every key. Within a version,
changes are additive only. Markdown receipts on stdout end with
<!-- runledger: safe=... schema=1 -->. - Risk for shell and database writes: files written through the shell get the same path
checks as edit-tool writes (secrets files high,.gitfolder medium, outside the project low).
New rules:db_migration(medium) for new or changed migration files,db_write(medium) for
migration runners andsqlite3/Python commands that change a local database, andtemp_copy
(low) for copying a project file into a temp folder. Read-only queries and scratch writes to
temp folders stay unflagged. - Test changes from the Git diff: each changed test file gets one verdict from its diff
against HEAD, however it was written: skipped, replaced, weakened or changed. Skipped and
weakened files add risk and count towardtest_tampering. Replaced and changed files appear in
a neutral quality signal,test_changes. A diff verdict replaces the per-step Edit rule for
that file, so a receipt no longer calls one file both weakened and replaced. - Receipts after the commit: when HEAD is newer than the session start (the run's changes
were committed beforerunledger receiptran), Git line counts and test verdicts compare the
working tree with the last commit before the session start instead of HEAD. JSON receipts
record that commit asgit_base(null when HEAD was used, including shallow clones and logs
without a start time). - Droid usage source: JSON receipts include
totals.usage_source(messages,settingsor
missing) andtokens_estimatedon each step. Markdown and HTML add a footer note when
per-step tokens are an even split of the session total.
Fixed
- Droid sessions run with
--cwd .: the real project folder is recovered from the session's
% pwdcontext, the settings file, the tool paths' Git work tree or the encoded session folder,
so these receipts get the branch, repo-relative files and Git line counts. JSON receipts add
cwd_source.listand--latestfind these sessions by their folder name. - Search patterns are not paths: the patterns and scripts of
grep,rg,ag,git grep,
sed,awkandperl -pe, andfind -name/-pathvalues, no longer count as paths, so
rg '/workspace|/home' srcis no longer flaggedshell_outside. Only operands in file
positions are checked (grep root /etc/passwdis still flagged), and other quoted regexes
with|,^,.*or groups are skipped. Quoted grep patterns no longer trigger the
dangerous-command rules (grep 'rm -rf /' .). - Relative working folders: a session whose log records
cwdas.(seen in Droid) no longer
takes the branch, Git line counts and checkpoints from the folder RunLedger runs in, and no longer
flags every absolute path as outside the working folder. - Safe mode and path lists: colon-separated path lists (
a:/home/u/b) are rewritten path by path. - Partial costs: when some tokens have no price, the run cost reads
≥ $X (N models unpriced)
and JSON addstotals.cost_partialandtotals.unknown_models. The cost-per-line quality signal
is skipped for partial costs, and advisor savings leave out steps with no token data. - Droid sub-agents and mixed models: sub-agent tokens from
inclusiveTokenUsageare counted,
credited to the child session's model when its settings file is found. Tokens that cannot be
credited to one model are listed asunattributedand no longer trigger the "add a price" help. - Large sessions: shell commands are parsed once per receipt instead of several times, so a
5,000-step session renders about twice as fast. - Droid tokens for copied transcripts: when a transcript has no sibling
.settings.json,
RunLedger finds the session's settings file by id under$FACTORY_HOME/sessions/. - Timestamps on Python 3.9 and 3.10: fractions with other than 3 or 6 digits (
00:01:00.5Z)
no longer make the run duration or a pushed run's time unknown. - Droid receipts without usage: token counts show
n/ainstead of 0, with a note naming the
missing settings file. Cost stays unknown.