Run log¶
The run log lets design partners measure the business hypothesis on their own sessions. It is local, off by default, and Inwards never sends it anywhere.
Turning it on¶
Either of these turns it on:
run-log = truein the root[tool.inwards];INWARDS_RUN_LOG=1in the environment the hooks run in.
INWARDS_RUN_LOG=0 turns it off even when the config says run-log = true. Either way, nothing is written in a project that doesn't use Inwards (no [tool.inwards] at its root and no session state), so a user-wide setting leaves other projects alone. inwards check --log logs that one run even when the log is off, which is how Aider's lint-cmd runs get recorded (add --log to the command init prints).
Lines go to .inwards/runs.jsonl, which inwards init keeps out of git for every agent. The file is readable by you only. inwards check logs next to the config it uses, so in a monorepo, a check run inside a package writes to that package's .inwards/. At 5 MiB the file moves to .inwards/runs.1.jsonl, replacing the previous one, and a new file starts. Two runs rotating at the same moment can lose that older file; the chance is small, and nothing else is lost.
Line schema inwards/run@1¶
Each line is one JSON object:
| Field | Type | Meaning |
|---|---|---|
v |
number | Schema version, 1. A field is only ever added; a change in meaning bumps v |
at |
string | ISO 8601 time the run ended |
session_id |
string or null | Claude Code session id; null for inwards check |
event |
string | SessionStart, PreToolUse, PostToolUse, Stop or check |
tool |
string or null | The tool of a tool event, e.g. Edit |
files |
string[] | Project-relative files the run checked. For check, its path arguments, or "." for the whole project |
lines |
object[] | PostToolUse only, for the file the hook checked: { "file", "added", "removed" }, counted from the tool call without the lines the edit repeats unchanged around its change. A Write counts every line as added; replace_all counts one occurrence |
fingerprints |
string[] | One per violation reported (rule code, module and message, hashed), the same as in the session state. Two identical imports in one file give the same fingerprint twice |
codes |
string[] | The rule code of each fingerprint, in the same order (e.g. INW001). Lines written before this field existed lack it |
severities |
string[] | error or warning for each fingerprint, in the same order, as [tool.inwards.rules] set it. Lines written before this field existed lack it and are read as errors |
suppressed |
number | Findings an inline suppression comment hid in this run and the run honoured. Lines written before this field existed lack it |
rejected |
string[] | Fingerprints whose inline suppression the hooks didn't honour, because the agent added it (agent-suppressions = "deny"); they are in fingerprints too. Lines written before this field existed lack it |
exit |
number | The exit code Inwards returned |
durationMs |
number | Time since the process started, process startup included, to 0.1 ms |
{"v":1,"at":"2026-09-25T20:14:03.512Z","session_id":"7a425a88-...","event":"PostToolUse",
"tool":"Edit","files":["shop/domain/order.py"],
"lines":[{"file":"shop/domain/order.py","added":2,"removed":1}],
"fingerprints":["4c1f0e9a2b7d3e10"],"codes":["INW001"],"severities":["error"],
"suppressed":0,"rejected":[],"exit":2,"durationMs":24.8}
Reading it¶
inwards stats [DIR] reads every .inwards/runs.1.jsonl and .inwards/runs.jsonl in the project: the one at the root, where the hooks write, and the one next to each config, where inwards check --log writes. It merges them in time order and prints the three numbers, each next to its chapter 2 threshold. The project is DIR, else CLAUDE_PROJECT_DIR, else the git work tree you are in, else the nearest folder above you with the hooks' .inwards/state, else the outermost one with a run log, stopping below your home directory. --config doesn't apply: stats always reads the whole project. --format json prints the same numbers as inwards/stats@1, with a met verdict for each. Nothing leaves the machine. To share the log itself, inwards stats --export FILE --redact writes every line of the project's logs in time order to one file, with each path and fingerprint replaced by a hash keyed by .inwards/export-key; see the design-partner guide.
Run log: 2 sessions, 6 hook runs, 1 unreadable line skipped.
Fixed within one retry: 1 of 2 (50%). Target: at least 80%. Not met.
INW001 1 of 1 (100%)
INW011 0 of 1 (0%)
unknown 0 of 0, 1 without a retry
1 more had no later run for their file and weren't reported at Stop.
Violations per 1,000 agent-written lines: 60 (3 in 50 lines). Target: at least 1. Met.
Hook latency: p50 40 ms, p95 200 ms over 6 runs. Target: p50 under 100 ms. Met.
What it counts:
- Fixed within one retry: a violation counts once per session and file, at the first
PostToolUseline that reports it, and only if it is an error: warnings don't block the agent. It is fixed when the nextPostToolUseline for that file no longer has its fingerprint, so every copy of it has to be gone. With no later run for the file, a laterStopline of the same session that still reports it counts as not fixed, so an agent that gives up doesn't drop out of the rate. Anything else is listed as without a retry. The per-rule split usescodes; a fingerprint never logged with a code counts underunknown. - Violations per 1,000 agent-written lines: error fingerprints first reported in a session's
PostToolUselines, once per session and file as in the retry rate, over the sum ofaddedin those lines. A.pyand its.pyishare a module, so a violation mirrored in a stub counts twice, once per file. - Hook latency: p50 and p95 of
durationMsoverPostToolUselines that checked a file, by the nearest-rank method. Edits to other files (Markdown, files outside the project) run the hook too, but check nothing, so they're left out. - Verdicts compare the exact ratios with the thresholds, not the rounded percentages.
- Rejected suppressions: distinct violations, per session, whose inline suppression the agent added and the hooks rejected, from
rejectedin any line. The text report shows the line only when there are some; JSON always hasrejectedSuppressions. It has no threshold: it counts how often the agent tried the cheapest dodge (ADR-028).
A hook checks the whole file, so its fingerprints include violations that were there before the agent touched it. Two kinds are left out of both rates:
- violations a
checkline reported before the session's first edit. Runinwards check --format json --logbefore you ask the agent for its first change; without it, violations already in a file count as the agent's the first time it edits that file; - violations the file's last hook run in another session had, so a violation left over from yesterday's session, from before
/clear, or from another agent working on the same file at the same time isn't counted as this session's, until a run on the file no longer has it. If it comes back after that, it is this session's.
Hook adoption, the share of installs with an agent hook, isn't in the log: one project can't see the others. Partners report it.