Skip to content

Releases: kodji7202-code/runledger

RunLedger 0.7.0

Choose a tag to compare

@github-actions github-actions released this 20 Oct 13:05
d340367

[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 --cwd or may be an absolute path inside the repository; matching is
    literal, so src restores src/x.txt but never src2/y.txt, and names with [ or * behave.
    Directories restore their whole subtree, including the deletion of files created after the
    checkpoint (labelled delete, 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, .git and 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 (so runledger rollback <safety-id> undoes a partial restore), the stale-preview check
    still covers the whole tree, and the symlink, junction, .git overlap, reserved-name and
    case-collision refusals apply to an in-scope path exactly as before. --path combines with a
    checkpoint id, --run, --last, --dry-run and --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 by runledger.risk): a
    destructive tool (delete_project, drop_table, reset_branch, purge_bucket) is a high
    mcp finding with the reason Called 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, so deleted_items and undelete_items stay 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 carries sql, query, statement or execute, or that carries a
    query/sql/statement argument that opens like a statement, is graded by that statement
    instead of its verb: DROP, TRUNCATE, a DELETE or UPDATE without a WHERE and
    ALTER TABLE ... DROP are 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, so select '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 --delete and
    git push origin :branch (high), find ... -delete and find ... -exec rm|unlink|shred,
    docker system|builder|image|container|network prune, gh <resource> delete, aws s3 rm of a
    single object, rsync --delete, dd of=<file> and reg delete of a user key (medium),
    helm uninstall, gh repo delete, docker volume prune and docker system prune --volumes,
    aws s3 rm --recursive, aws s3 rb --force, dd of=/dev/..., rd /s /q, a shell
    DELETE FROM without a WHERE and reg delete of a machine-wide key (high), plus PowerShell
    Remove-Item -Recurse without -Force (medium; the existing -Force rule 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 mentions git branch -D is not flagged.
  • Rules you write yourself (command_rules, tool_rules, mcp_verbs): the user file
    (~/.runledger/config.json) and a project's .runledger.json may now carry three top-level
    keys. command_rules.deny/.ask take regular expressions, or objects with pattern, an
    optional reason and raw; they are searched case-insensitively against the same
    quote- and heredoc-blanked view the built-in command rules use, and raw: true reads the whole
    command instead. tool_rules.deny/.ask take fnmatch globs 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/.read add words to the MCP verb vocabularies, so a server
    whose destructive tool is named something else is graded as expected. Hits are rule_deny
    (high) and rule_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.read list, 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 Droid Execute
    is decided as the shell call it is, Create/Edit as 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 as Search and FetchUrl as WebFetch; MCP names keep
    both shapes (mcp__server__tool and Droid's bare server__tool). The log keeps the agent's own
    tool name and adds agent and canonical_tool (Execute becomes Bash), and session_id stays
    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 under Bash (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 --agent is recognised as
    RunLedger's own by install, uninstall and 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 install for 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 no hooks wrapper and never its
    settings.json; Codex gets <project>/.codex/hooks.json / <CODEX_HOME>/hooks.json, with the
    events wrapped in hooks, matcher .* and config.toml never touched. The guard command carries
    the agent ("<python>" -m runledger guard --agent droid|codex; --portable keeps the bare form),
    the checkpoint hooks stay runledger checkpoint --hook for all three, and install runs the
    stored command once. Installs are idempotent (one guard entry per file, <file>.bak written 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;
    uninstall removes only RunLedger's entries plus the groups, events and hooks object they
    emptied, and an agent without a hook file is reported without creating one. --agent all installs
    in a project for all three and, globally, only for agents whose config folder already exists (it
    never creates ~/.factory or ~/.codex, and names each skipped agent with the reason). guard test --agent droid|codex prints 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...
Read more

RunLedger 0.6.1

Choose a tag to compare

@github-actions github-actions released this 19 Oct 07:05
4bf048d

[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 keep cost_usd null on every model of
    such a run instead of the 0.0 the priced-model bucket used to carry, so a model group made only
    of missing-usage runs shows cost_usd null with priced_runs 0 in GET /api/stats and
    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/insights now returns priced_runs, partial_runs and cost_is_lower_bound, so an agent
    whose runs are all only partly priced reads ≥ $X in the dashboard instead of a complete figure.

  • The compliance report marks a partly priced developer as a lower bound (runledger.server): a
    developer row in GET /api/export/report.html reads ≥ $X when 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/a for an unpriced month and ≥ $X for 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 their tests_run credit again, as they did in v0.6.0:
    pytest -k add, pytest -k "install and not slow", npm test -- --grep install and
    jest -t uninstall are runs. Inspection flags (--version, --help, --collect-only,
    --list-tests and 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.com or C:\Users\ana\app\x.py now 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.md and 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 --ai writes them as JSON (runledger.llm, runledger.summarize):
    a
    quoted path such as "C:\Users\Jane Smith\app\x.py" or /Users/Jane Smith/app/x was masked only
    up to the first word, leaving Smith in 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 named Jane (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 a MultiEdit's
    edits (and the doubled Windows separators that JSON produces) can no longer carry a complete
    home path into an --ai request. The check at the API boundary also recognizes a path whose
    separators were doubled, and command boundaries are kept: cat ~/app/x && ls still 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 a runledger backup running beside
    the server cannot hold the write lock and block pushed runs. runledger backup copies 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/insights returns
    the quality.by_grade buckets 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.json is tighten-only in receipts as well as the guard: ignore and
    lowering severity_overrides are read only from your user config (~/.runledger/config.json or
    RUNLEDGER_CONFIG). A project .runledger.json may 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 install writes interpreter-bound hook commands by default: guard install
    writes "<sys.executable>" -m runledger guard and "<sys.executable>" -m runledger checkpoint --hook
    so the hooks run without runledger on PATH and when the Python path contains spaces, upgrades
    earlier bare runledger ... hook entries in place, and verifies the configured command exits 0
    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 --portable to
    write bare runledger ... 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 receipt now enables safe mode by default for HTML
    files written inside a Git repository (matching Markdown and JSON; pass --no-safe for full
    paths), and when -o is omitted it writes runledger-<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): the 401
    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, with org.opencontainers.image.version and
    org.opencontainers.image.revision fed by the RUNLEDGER_VERSION and RUNLEDGER_REVISION build
    arguments (the CI docker job passes the version from pyproject.toml and the commit it built, then
    checks the labels on the ...

Read more

RunLedger 0.6.0

Choose a tag to compare

@kodji7202-code kodji7202-code released this 16 Oct 04:34
cc4dd08

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 or RUNLEDGER_SAFE_PREFIXES prefixes) and mask e-mail addresses and URL
    credentials. HTML and files outside a repository keep full paths. --safe and --no-safe
    override the default; JSON receipts add a safe key. 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 and docs/receipt-json.md describes 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, .git folder medium, outside the project low).
    New rules: db_migration (medium) for new or changed migration files, db_write (medium) for
    migration runners and sqlite3/Python commands that change a local database, and temp_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 toward test_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 before runledger receipt ran), 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 as git_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, settings or
    missing) and tokens_estimated on 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
    % pwd context, 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. list and --latest find these sessions by their folder name.
  • Search patterns are not paths: the patterns and scripts of grep, rg, ag, git grep,
    sed, awk and perl -pe, and find -name/-path values, no longer count as paths, so
    rg '/workspace|/home' src is no longer flagged shell_outside. Only operands in file
    positions are checked (grep root /etc/passwd is 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 cwd as . (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 adds totals.cost_partial and totals.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 inclusiveTokenUsage are counted,
    credited to the child session's model when its settings file is found. Tokens that cannot be
    credited to one model are listed as unattributed and 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/a instead of 0, with a note naming the
    missing settings file. Cost stays unknown.