Skip to content

Run logs

Every run that changes something, or runs long, leaves one plain-text file behind (D-077). It exists for the case a terminal cannot cover: an install that ran for hours and closed its window, a dry run that sat silent while it asked a publisher a thousand questions, an issue that needs "what did it actually do". Read it with hammunition logs; it is not the record hammunition uninstall stands on, which is the transaction log.

Which commands

Logged: install, uninstall, update, menus apply, hardware apply, hardware unapply, hardware park, hardware wake, every maps ..., reference serve, services ... and time mode, with or without --dry-run.

Not logged: the readouts (status, list, show, doctor, hardware list, hardware state, artifacts, logs, station show, reference books, time) and station set, whose argv is the station values.

Where, and who can read it

$XDG_STATE_HOME/hammunition/logs/, ~/.local/state/hammunition/logs/ by default: the same state directory as the transaction log, and resolved the same way. A run under sudo logs under the invoking operator's state directory, never root's, and hands the directory and the file to them. The directory is mode 0700 and each file 0600: a log can hold command output and paths.

The file is named <UTC timestamp>-<command>-<pid>.log, for example 20261002T181500Z-install-12345.log.

Format

One event per line: <UTC timestamp, milliseconds> <tag> <text>. Written and flushed a line at a time, so a run that was killed leaves a log you can read up to the moment it died.

Tag What it is
meta The run's own facts: engine version, command, argv, pid and euid. The value of --callsign, --grid-square, --node-alias, --map-regions, --rig-device, --rig-owner and --mirror is replaced by <redacted>, and so is every saved station value (callsign, grid, alias, regions, mirror and its host, rig device and owner) wherever it appears in the lines below, so a plan that prints a region does not carry it into the file. A crash adds crashed: and the traceback.
out, err Everything the engine printed on stdout and stderr, the plan included. The terminal still sees exactly the same bytes. A progress counter redrawn in place leaves its first and last lines, not a line per redraw.
cmd A command the engine started, as run.
cmd-out, cmd-err That command's stdout and stderr, line by line as they arrive, so tail -f follows an hour-long build.
cmd-end exit=<code> elapsed=<seconds> for that command.
result The last line: exit=<code> <ok\|failed\|refused\|not confirmed> elapsed=<seconds>, the same codes as the CLI.

A real run, from a fixture (a failing command, a station flag in argv; the interpreter path in the cmd line shortened):

2026-10-02T18:48:05.836Z meta    hammunition 0.19.0
2026-10-02T18:48:05.836Z meta    command: install
2026-10-02T18:48:05.836Z meta    argv: hammunition install fixture-apt --callsign <redacted>
2026-10-02T18:48:05.836Z meta    pid=974784 euid=1000
2026-10-02T18:48:05.836Z out       $ apt-get install --yes -- fixture-apt
2026-10-02T18:48:05.836Z cmd     $ apt-get install --yes -- fixture-apt
2026-10-02T18:48:05.851Z cmd-err E: Unable to locate package fixture-apt
2026-10-02T18:48:05.851Z cmd-out Reading package lists...
2026-10-02T18:48:05.854Z cmd-end exit=100 elapsed=0.0s  apt-get
2026-10-02T18:48:05.854Z err     error: apt-get install failed (exit 100)
2026-10-02T18:48:05.854Z result  exit=1 failed elapsed=0.0s

Only the commands the engine runs through its command runner are streamed as cmd-out; steps the engine performs itself (a download, an unpack) appear as the out lines it prints for them.

Each file stops at 50 MB (reference serve can run for days): one log truncated line, then only the result line is added. services with no verb, --json runs that are not dry runs (a front end polling update --json) and the readouts leave no log, so they cannot push real logs out of the 30.

Scrubbing is exact-text and case-insensitive, for values of three characters or more. It is not a guarantee about command output: a tool that prints something of yours in a form the engine does not know is logged as it printed it. Read a log before attaching it to an issue.

Rotation

At the start of each logged run the oldest logs are removed until the new one fits under 30 files and 200 MB in total. A run in progress is never removed: each run holds a lock on its own file for as long as it lives, which the kernel releases however the process ends, so a killed run is not mistaken for a live one and a live one is not deleted. Files in the directory whose names are not run logs are not counted and not touched.

Both limits are constants (MAX_FILES, MAX_BYTES in src/hammunition/runlog.py), not station settings: station values are what only the operator can supply, and a retention policy is not one.

Reading them

hammunition logs            # when, which command, size, how it ended
hammunition logs --last     # the newest, in full
tail -f "$(hammunition logs --path)"
hammunition logs --json     # for a front end; see json-interface.md

How a run ended: ok, failed, refused, not confirmed (the exit code's words); running while a live process holds the file; incomplete when there is no result line and nothing holds the file, which is a run that was killed. hammunition doctor reports how many logs there are, their size, and how the newest ended. A run that ends on a terminal prints Log: <path> as its last line, on stderr; under --json it does not (the document is unchanged, and stderr is diagnostics for it), and the run is still logged.

If the log cannot be written

The run says so once on stderr (note: no run log for this command ...) and carries on unlogged. A log must not fail the run it describes.

What was not measured

A run under real sudo handing the directory and file to the operator uses the same helpers as the transaction log and is tested with an injected euid, not on a machine.

To undo all of it, delete the directory: nothing reads the logs back but hammunition logs and doctor.