Every Skrog command that reports state can emit machine-readable JSON with
--json. This is the contract that tools build on — the
VS Code extension, CI scripts,
fleet health checks — so it is governed by three rules:
- Additive only. Fields are added, never renamed or removed. A consumer that ignores unknown fields keeps working across Skrog versions.
- Exit codes mean the same thing as the human output —
0ok,1error,2usage,3not installed / not found,4unsupported platform (this machine cannot run what was asked for and no retry will change that: the manifest has no engine rootfs built for this CPU architecture. amd64 and arm64 both do, so in practice this means neither, #388) — and a non-zero exit still emits the JSON when there is something to say (e.g.version --jsonexits 3 with no engine,cli status --jsonexits 3 with tools missing). - Arrays are arrays. An empty list is
[], nevernull. Wherenullis used it is deliberate and documented (config --json→engine).
Shapes are pinned by cmd/skrog/jsonshapes_test.go.
{
"installed": true,
"distro": "skrog-engine",
"stateDir": "C:\\Users\\me\\AppData\\Local\\Skrog",
"supervisor": "running",
"engine": "running",
"desired": "running",
"profile": "work",
"gpu": { "enabled": true, "probed": true, "visible": true, "specInstalled": true },
"endpoint": {
"pipe": "\\\\.\\pipe\\docker_engine",
"dockerHost": "npipe:////./pipe/docker_engine",
"reason": "default pipe is free"
}
}supervisor:running|stopped.engine:running|idle|stopped.idleis the engine stopped by the idle timeout, or by awsl --shutdownthe supervisor saw happen (#518) — healthy, it wakes on the nextdockercall. Scripts can tell it from broken.desired:running|stopped(whatskrog start/stoplast asked for).profile: omitted when no profile is active.target: the named engine this status describes (targets.md); omitted for the default shared engine. Without--state-dir, status describes the engine docker is on now, so a consumer that follows the docker context reads the engine it is talking to.gpu:visibleandspecInstalledare only probed while the engine is running andenabledis true — status never boots a stopped distro (#82).probed: falsemeans they are not authoritative.endpoint: where the engine is answering, and absent unless a supervisor is running — nothing is being served then, so there is no endpoint to name.pipeis the named pipe;dockerHostis the same thing spelled the way the docker CLI wants it, so a consumer settingDOCKER_HOSTdoes not have to convert between the two.reasonsays why that pipe: the default one was free, or something else (usually Docker Desktop) already had it.- It reports what the running supervisor bound, read back from its own record — never recomputed. Docker Desktop can start or stop after Skrog chose, so asking the selector again could name a pipe nothing is serving (#273).
Exit 0 always (an uninstalled machine is installed: false, not an error).
{ "app": "0.3.0", ... } — the full component picture; see skrog version.
Exits 3 when no engine is installed (still emits JSON).
engine carries both identifiers of the installed image, and they answer
different questions:
"engine": {
"version": "29.8.1",
"engineRef": "29.8.1-3",
"rootfsSha256": "f1be49d9…"
}versionis dockerd's version, and two images can share it. It does not say which image you have.engineRefis the image: version plus rootfs revision. It is the unitskrog engine upgrademoves between, so it is the field to compare when asking "is this machine current". Added in 0.8.0; omitted on an install with no recorded ref and a rootfs URL it cannot be derived from.rootfsSha256identifies the exact bytes, and is what--rootfs-sha256and a bug report speak. Unchanged.
engineRef was added rather than folded into either of the others, because a
reader switching on version or rootfsSha256 has to keep working.
{
"app": "0.4.0",
"worst": "warn",
"results": [
{
"name": "wsl-version",
"title": "WSL default version",
"status": "ok",
"summary": "default version is 2",
"detail": ["..."],
"remedy": "wsl --set-default-version 2",
"fixed": "set the default WSL version to 2"
}
]
}An object, not an array: worst saves a consumer from folding the
statuses itself, and app identifies the build that ran the checks. The
results are under results. status is one of ok | skip | warn |
fail; detail, remedy and fixed are omitted when empty, and fixed
records what --fix did on this run.
Exit code is 0 when every check passed or only warned, 1 when one or
more failed, 2 usage — the same three doctor --help states. A warning is
not a failure: doctor warns about things worth knowing that do not stop the
engine working, so gating a fleet script on a non-zero exit would report
healthy machines as broken.
Both of these were documented wrongly here until v0.4.0 (#238) — as an array, and with
1meaning warn. A consumer written against the old text iterated object keys and treated a failing doctor as a usage error.
The same numbers status --stats --json reports are also available as
Prometheus text for node_exporter's textfile collector — see
monitoring.md. It is the one machine-readable output here that
is not JSON, and its exit code deliberately differs (always 0 when metrics
could be written, because the engine's state is in the metrics).
{
"settings": { "idle-timeout": "5m0s", "audit": "on", "network.proxy": "", "gpu": "off", "...": "..." },
"engine": { "engine.registry-mirrors": "https://mirror.corp", "...": "..." }
}settings: every known key with its stored value or default — a stable key set.autostartis the one exception to "stored": it is the recorded choice (on/off), or, on an install that never recorded one, what the logon Run entry actually says — never an empty value for a setting with a real state (#515). Whether the entry is registered right now isskrog autostart status, and a mismatch between the two is whatskrog doctorreports.
engine: the engine'sdaemon.jsonkeys.nullwhen no engine is installed;{}when installed with nothing set. The two are different answers.
{
"arch": "amd64",
"binDir": "C:\\...\\Skrog\\bin",
"onPath": true,
"activeDocker": "C:\\...\\Skrog\\bin\\docker.exe",
"tools": [
{ "name": "docker", "version": "29.8.0", "role": "cli", "path": "...", "installed": true, "available": true },
{ "name": "compose", "version": "5.5.1", "role": "plugin", "path": "...", "installed": true, "available": true }
]
}available: whether the manifest publishes the tool for this arch at all. Every tool is available on both architectures today; the field exists because that has not always been true and need not stay true — a component with no build for the host reportsfalserather than failing at install.activeDocker: thedockerthat resolves on PATH; omitted if none.- Exits
3when an available tool is not installed.
snapshot list --json→ array (always) of{ "name", "created", "engineVersion", "distro", "sha256", "sizeBytes" }.snapshot save <name> --json→ one such object.snapshot restore <name> --yes --json→{ "restored": "<name>" }.snapshot delete <name> --json→{ "deleted": "<name>" }.
Flags come before the verb: skrog snapshot --json list.
profile --json(list) →{ "active": "work", "profiles": [ { "name": "work", "active": true }, … ] }(activeomitted when none;profilesalways an array).profile show <name> --json→ the profile as the same document its YAML holds:distro,data-dir,engine-version,idle-timeout,autostart,engine,hooks,integrations(unset fields omitted).
The audit log is already JSON lines — one audit.Event per line:
{"time":"2026-09-09T16:46:39.226Z","action":"image-pull","method":"POST","path":"/v1.44/images/create","image":"nvidia/cuda:12.4.1-base-ubuntu22.04","status":200,"ms":1840}Fields: time, action, method, path, image, name, container,
status, ms, error (optional ones omitted when empty). Records are derived
from the request line only, never the body.
- Default output: JSON lines (one parse per line; friendly to
tail -f). audit tail --json: the same records as one JSON array, for a single parse.[]when the log does not exist yet.
The resulting install manifest: { "distro", "dataDir", "rootfsUrl", "rootfsSha256", "engineVersion", "installedAt", "wslVersion" }.
-
target --json(list) →{ "current": "proj-a", "targets": [ { "name": "local", "kind": "local", "current": false, "state": "running", "context": "skrog", "distro": "skrog-engine", "stateDir": "C:\\...\\Skrog", "engineVersion": "29.9.0", "diskBytes": 1395864371 }, { "name": "proj-a", "kind": "engine", "current": true, "state": "running", "context": "skrog-proj-a", "distro": "skrog-proj-a", "stateDir": "C:\\...\\Skrog\\engines\\proj-a", "pipe": "\\\\.\\pipe\\skrog-proj-a", "engineVersion": "29.9.0", "diskBytes": 641728512, "autostart": false }, { "name": "desk", "kind": "remote", "current": false, "context": "skrog-desk", "host": "tcp://desk.corp:2376" } ], "totalDiskBytes": 2037592883 }currentis"local"when docker is on theskrogcontext, a named engine's or remote's name when onskrog-<name>, and""when docker is on some other context entirely.kindislocal(the default shared engine, always listed),engineorremote.stateisrunning|stopped|incomplete(no install recorded, or its distro is gone) |not-installed, and is absent for a remote.diskBytes,engineVersion,distro,stateDirandpipeare absent for a remote;autostartis present for named engines only.targetsis always an array, andtotalDiskBytesis what the local engines hold together. -
target create <name> --json→{ "name", "stateDir", "distro", "pipe", "context", "engineVersion", "dockerContext" }.dockerContextisfalseon a machine with no docker CLI to hold the context. Exits2for a bad or reserved name,1when the name is taken (by an engine or a remote). -
target remove,useandautostart --enginehave no JSON; they exit0on success and3when there is no such target.
-
remote --json(list) →{ "current": "desktop", "remotes": [ { "name": "desktop", "host": "tcp://my-desktop.corp:2376", "added": "…", "certNotAfter": "2028-09-09T16:00:00Z", "dir": "C:\\...\\remotes\\desktop", "current": true } ] }currentis"local"when docker is on theskrogcontext, a remote's name when onskrog-<name>, and""when docker is on some other context entirely.remotesis always an array. -
remote test <name> --json→{ "name", "serverVersion", "ms" }. Exits1when the remote does not answer,3when no such remote.
Runs the command, then reports what it did to the engine from the audit records appended while it ran:
{
"command": ["act", "-j", "build"],
"exitCode": 0,
"ms": 41200,
"events": 9,
"actions": { "image-pull": 2, "container-create": 3, "container-start": 3, "exec-start": 1 },
"images": ["catthehacker/ubuntu:act-latest", "node:20"],
"containers": ["act-build-1a2b", "db"],
"note": "audit log rotated during the run; the summary covers the current file"
}exitCodeis the traced command's own; the process exits with it too, soaudit trace -- make testfails exactly whenmake testdoes.images/containersare the distinct ones touched — always arrays.noteis omitted unless something qualifies the summary (rotation mid-run, or no records written at all).- Attribution is by log position (appended after the command started), so concurrent docker use during the run is included.
- Requires
auditto be on; otherwise exits1with the recipe. --rawinstead prints the matching records as JSON lines.
A readiness probe for runner warm-ups and orchestrators:
{ "installed": true, "supervisor": "running", "engine": "idle", "ready": true,
"reason": "engine idle; wakes on the next docker command" }readyis the verdict the exit code carries:0ready,1not ready,3not installed.reasonis never omitted.- Ready means a docker command would succeed now: the supervisor is serving the
pipe and the engine is
runningoridle(idle wakes on demand). --wait <duration>keeps probing until ready or the deadline; nothing is started by the probe itself — pair it withskrog start.
One object per line, the same envelope for every source so a log shipper needs one pipeline:
{"source":"dockerd","line":"time=\"2026-09-09T16:44:18Z\" level=info msg=\"Daemon has completed initialization\""}
{"source":"supervisor","line":"time=... level=INFO msg=\"engine socket is up\" distro=skrog-engine"}
{"source":"audit","line":"{\"time\":\"...\",\"action\":\"image-pull\",...}"}--source supervisor|dockerd|audit(default supervisor);-n <lines>(default 200,0= all);--followstreams new lines and survives the 5 MB rotation.lineis the raw record; parse it further if you want dockerd's logfmt fields or the audit event's JSON.- Exits
3for--source dockerdwith no engine installed.
Pulls a pinned image list ahead of need (runner warm-up, golden-image bake, post-start hook), through whatever docker currently targets:
{
"file": "images.txt",
"concurrency": 3,
"pulled": 2,
"failed": 1,
"ms": 8420,
"images": [
{ "ref": "alpine:3.20", "ok": true, "ms": 1210 },
{ "ref": "node:20@sha256:…", "ok": true, "ms": 8390 },
{ "ref": "ghcr.io/x/missing:1", "ok": false, "ms": 640, "error": "manifest unknown" }
]
}imagesis in list order and always an array;erroris omitted on success.- Exit
0only whenfailedis0; a failed pull never stops the others. - The list file: one reference per line,
#comments and blank lines ignored, duplicates dropped. Digest pins encouraged.
One verdict on whether an unattended host will bring the engine back after a reboot (see auto-logon-runner.md):
{
"ready": false,
"findings": [
{ "name": "autologon", "status": "ok", "summary": "auto-logon is configured" },
{ "name": "autologon-account", "status": "ok", "summary": "auto-logon uses this account" },
{ "name": "autologon-password", "status": "warn",
"summary": "the auto-logon password is stored in clear text in the registry (Winlogon\\DefaultPassword)",
"remedy": "use Sysinternals Autologon, which stores it as an LSA secret, then delete the DefaultPassword registry value (docs/auto-logon-runner.md §3)." },
{ "name": "autostart", "status": "fail", "summary": "no logon autostart; the session will start but the supervisor will not",
"remedy": "run `skrog autostart enable` as the auto-logon account (needs skrogw.exe beside skrog.exe)." },
{ "name": "supervisor", "status": "ok", "summary": "supervisor is running" },
{ "name": "power", "status": "warn",
"summary": "the machine sleeps on mains power (sleeps after 1h0m0s), which suspends a job mid-run",
"remedy": "powercfg /change standby-timeout-ac 0 && powercfg /change hibernate-timeout-ac 0" },
{ "name": "engine", "status": "ok", "summary": "engine is running" }
]
}readyis the exit code's verdict:0ready (warnings allowed),1not ready,3not installed.findingsis always an array;statusisok|warn|fail;remedyis omitted whenok.powerreads the active scheme's AC timeouts only: a laptop on battery having short DC timeouts is correct, not a misconfiguration. It warns and never fails — many runners are desktops that will never sleep, and a check that fails a healthy host is one people learn to ignore. A reading that could not be taken warns as unknown rather than passing.- Read-only and unelevated. The auto-logon account is compared, never printed, and the password value is probed for existence only.
The runner's clean slate — snapshot restore with the interactive guards
implied (no --yes, no running-container check):
{ "snapshot": "golden", "engineVersion": "29.7.2", "ms": 6840 }msis the whole cycle — verify the archive, unregister, import, engine back — the number a clean-slate budget is measured against.engineVersionis the snapshot's recorded engine, omitted when unknown.- Exit
3when the snapshot does not exist or nothing is installed;1when the restore failed (the engine is brought back best-effort either way).
Reclaims disk on whatever docker currently targets:
{
"reclaimedBytes": 1234000000,
"failed": 0,
"steps": [
{ "name": "containers", "reclaimedBytes": 100000000 },
{ "name": "images", "reclaimedBytes": 1134000000 },
{ "name": "build-cache", "reclaimedBytes": 0, "error": "..." }
]
}stepsis in plan order (containers → images → volumes → build-cache) and always an array;erroris omitted on success. A failed step never stops the others.- Exit
0only whenfailedis0. --allremoves every unused image (default: dangling only);--until 168hkeeps anything newer;--build-cacheand--volumeswiden the sweep (volumes hold data, so off by default).
Shrinks the engine's virtual disk (fstrim + CompactVirtualDisk):
{
"distro": "skrog-engine",
"path": "C:\\Users\\me\\AppData\\Local\\Skrog\\distro\\ext4.vhdx",
"trimmed": true,
"offeredBytes": 1078939029504,
"beforeBytes": 15032385536,
"afterBytes": 9663676416,
"reclaimedBytes": 5368709120,
"waitedSeconds": 66.4,
"restarted": true,
"dryRun": false
}reclaimedBytesis the difference in the file's size on disk and the only honest measure of what happened.offeredBytesis whatfstrimprinted: the free extent of the whole virtual disk, not space reclaimed. Named "offered" so nothing mistakes it for a result; omitted when--no-trimwas used.heldis present when other distros are keeping WSL from releasing the disk, and pairs with exit code 11. Under--dry-runit appears without an error: the dry run reports that a real run would refuse, and names who.waitedSecondsis how long WSL took to let go (about a minute after the last distro stops).- Exit codes:
0ok,1error,2usage,3not installed,11the disk is held.
Moves the engine's data directory to another drive:
{
"distro": "skrog-engine",
"from": "C:\Users\me\AppData\Local\Skrog\distro",
"to": "D:\skrog",
"movedBytes": 439422976,
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"needBytes": 1098907648,
"freeBytes": 181070462976,
"restarted": true,
"dryRun": false
}movedBytesandsha256describe the transfer archive: the engine is exported, checksummed, and only then is the old distro unregistered, so the archive is the recovery point if the import fails.needBytesis the peak requirement on the target, roughly twice the current disk, because the archive and the imported disk exist at the same time. Checked before anything is touched; short space pairs with exit code 12 and changes nothing.- Under
--dry-runthe payload isstepsplusneedBytes/freeBytes— what it would do and whether it would fit. archiveKeptappears with--keep-archive, or when the archive could not be removed, and names the file to delete by hand.- Exit codes:
0ok,1error,2usage,3not installed,12not enough space.
Admission control (#120). show reports the rules in effect:
{
"path": "C:\Users\me\AppData\Local\Skrog\policy.yaml",
"exists": true,
"active": true,
"enforced": true,
"rules": { "denyPrivileged": true, "allowBindSources": ["C:\work"] },
"source": { "machine": "...", "user": "..." },
"machineProvenance": { "path": "...", "trusted": false, "reason": "..." }
}source says what each layer contributed, so "why can I not run this" has an
answer that names the file to argue with
(#386).
machineProvenance says where the machine layer came from and whether it was
trusted (#418). It has no
omitempty, so it is on every response. A fleet dashboard needs to tell a
machine with no policy from one whose policy was refused for bad ownership;
they used to be indistinguishable here, and the second is the one worth an
alert.
test is the verdict on one container-create body, for gating a script:
{
"denied": true,
"rule": "allow-bind-sources",
"reason": "policy does not allow bind mounts from C:/secrets (allowed: C:\work)"
}rulenames the key that refused, so a script can branch on it without parsing prose;reasonis the same sentence the user would see.active/enforcedare false when the file is absent or sets no rules — both mean every request is allowed.- Exit codes:
0allowed,1error,2usage,3not installed,13the rules deny it. 13 is separate so "refused" is distinguishable from "the command went wrong".
What this build can install, what is installed, and where a rollback goes:
{
"installed": "29.7.2-4",
"previous": "29.7.2-3",
"available": [
{ "ref": "29.7.2-4", "version": "29.7.2", "default": true, "published": true }
]
}refis the revisioned label and the unit an upgrade moves between;versionis only the dockerd version, and two revisions can share one.publishedisfalsefor a manifest entry with no checksum yet — a placeholder that cannot be installed.installedandpreviousare omitted when unknown;availableis always an array.
{
"from": "29.7.2-3",
"to": "29.7.2-4",
"replaced": ["dockerd", "containerd", "runc", "..."],
"engineVersion": "29.7.2",
"rolledBack": false,
"dryRun": false
}engineVersionis what the new dockerd reports about itself — evidence the swap took, not an assumption that it did.rolledBack: truewith a non-zero exit is the interesting case: the upgrade failed and the previous engine was restored, so the engine is up.replacedandengineVersionare absent on a dry run.
The WSL2 VM''s sizing, from the global ~/.wslconfig (#148):
{
"path": "C:\\Users\\me\\.wslconfig",
"exists": true,
"effective": { "memory": "4GB", "processors": "2", "autoMemoryReclaim": "gradual" },
"desired": { "memory": "4GB", "processors": "2", "autoMemoryReclaim": "gradual" },
"pending": [ { "key": "memory", "old": "8GB", "new": "4GB", "added": false } ],
"applied": false
}effectiveis what the file says now;desiredis what Skrog's own settings ask for;pendingis the difference — so a converge script can tell "already right" from "would change something" without parsing prose.pendingis omitted when there is nothing to do, which is the signal that a repeatedapply --yesis a no-op.appliedistrueonly when this invocation wrote the file.apply --jsonrequires--yes: there is no way to ask a question in JSON, so it exits2rather than appearing to hang.
--stats adds a stats object; the rest of the shape above is unchanged,
because it is a readiness-probe contract. Statistics are opt-in for two
reasons: collecting them costs WSL calls a probe should not pay, and they are
only meaningful when the engine is already running.
{
"installed": true, "distro": "skrog-engine", "engine": "running",
"stats": {
"probed": true,
"supervisor": {
"fresh": true, "readingAgeSeconds": 0.2,
"uptimeSeconds": 3600, "engineUptimeSeconds": 3600,
"engineStarts": 1, "idleStops": 2,
"lastIdleStopAt": "2026-09-10T14:00:00Z", "lastWakeAt": "2026-09-10T14:31:00Z"
},
"bridge": {
"connections": 512, "bytesToEngine": 3281, "bytesToClient": 12883,
"activeConns": 0, "transport": "vsock"
},
"engine": {
"version": "29.7.2", "containers": 1, "containersRunning": 1,
"containersPaused": 0, "containersStopped": 0,
"images": 9, "volumes": 2,
"imagesBytes": 12191537, "volumesBytes": 0,
"buildCacheBytes": 0, "reclaimableBytes": 490
},
"disk": {
"path": "C:\\Users\\me\\AppData\\Local\\Skrog\\distro\\ext4.vhdx",
"sizeOnDiskBytes": 415236096, "guestUsedBytes": 305328128,
"reclaimableBytes": 109907968, "hostFreeBytes": 428330541056
},
"vm": {
"cpus": 20, "memTotalBytes": 33481715712,
"memAvailableBytes": 31422464000, "swapTotalBytes": 8589934592,
"configuredMemory": "4GB", "configuredProcessors": "2"
}
}
}probedsays whether the engine was up. Statistics never start it (#82), soprobed: falseomitsengine,diskandvmentirely rather than reporting zeroes — "no data" and "zero containers" must not look alike.supervisorandbridgecome from a file the supervisor flushes, so they are present even with the engine down.freshandreadingAgeSecondssay whether they still describe the present: a supervisor that died leaves its last numbers behind, andfresh: falseis how a reader knows not to trust them as current.bridge.transportisvsock(fast path, ~0.6 ms per connection),fallback(the socat relay, ~165 ms — the vsock agent is unreachable),socat(that path pinned bySKROG_NO_VSOCK) orunknown. This is the field that explains a slowdockerwith a perfectly healthy engine.engine.reclaimableBytesis whatskrog prunecould free;disk.reclaimableBytesis size-on-disk minus guest-used, roughly whatskrog compactcould return — an estimate, since compaction works in blocks.vm.configured*is what~/.wslconfigasks for, omitted when it asks for nothing. Comparing it withmemTotalBytesandcpuscatches the trapskrog wsl-configcloses: a limit recorded and never applied.errorsnames anything that could not be read, so a partial reading is honest rather than silently short.
One reading of where the VM's memory and CPU go (memory.md). Groups are abbreviated here; every group has the same keys as the container shown.
{
"engine": "running",
"distro": "skrog-engine",
"reading": {
"takenAt": "2026-09-22T20:53:31Z",
"windowSecs": 1.2,
"vm": {
"cpus": 4, "cpuPercent": 9.5,
"memTotalBytes": 8171814912, "memFreeBytes": 7362633728,
"memAvailableBytes": 7618048000, "memUsedBytes": 809181184,
"anonBytes": 72073216, "pageCacheBytes": 396902400, "kernelBytes": 79104000,
"unitemisedBytes": 261101568,
"pressure": { "cpu": 0.14, "memory": 0, "io": 0.44 }
},
"containers": [
{
"id": "8e76bca2ec40…", "name": "web",
"memoryBytes": 15872000, "anonBytes": 3911680,
"fileBytes": 10186752, "kernelBytes": 1155072,
"cpuPercent": 0, "ioReadBytes": 10104832, "ioWriteBytes": 8192,
"limitBytes": 268435456, "pids": 5,
"pressure": { "cpu": 0, "memory": 0, "io": 0.36 }
}
],
"engine": { "name": "engine", "memoryBytes": 266420224, "…": "…" },
"otherDistros": { "name": "other distros", "memoryBytes": 0, "…": "…" },
"otherDistroCount": 0,
"wsl": { "name": "wsl", "memoryBytes": 884736, "…": "…" },
"otherContainers": { "name": "other containers", "memoryBytes": 0, "…": "…" },
"unchargedBytes": 525766656,
"vmmem": {
"process": "vmmem", "pid": 23688,
"workingSetBytes": 1061056512, "privateWorkingSetBytes": 1061056512,
"privateBytes": 1063284736
}
}
}engineat the top level is the state, instatus's vocabulary:running,idleorstopped.readingis present only while it isrunning—topnever starts the engine to take one (#82), and a zeroed reading would look like an empty VM.cpuPercentis of one CPU, likedocker stats: two busy cores read200. It is averaged overwindowSecs, which is--interval.limitBytesis the group'smemory.max, omitted when there is no limit — not the VM total, whichdocker statsreports as the limit of every unconstrained container.pidsispids.current.pressureis PSIsomeavg10, in percent: the share of the last ten seconds in which work was stalled on that resource.memUsedBytesis total minus free, page cache included — what Windows has to back. It is exactlyanonBytes + pageCacheBytes + kernelBytes + unitemisedBytes.kernelBytesis what/proc/meminfoitemises as the kernel's (Slab, KernelStack, PageTables, SecPageTables, VmallocUsed, Percpu);unitemisedBytesis the rest, which no meminfo field names — derived, and on WSL not small (driver allocations, by inference).unchargedBytesis derived, not measured: used memory no top-level cgroup accounts for.engineinsidereadingis the engine distro's own group (dockerd, containerd, BuildKit, shims), which is where pull and build page cache lands.vmmemis omitted when the process could not be identified — for example several Hyper-Vvmmemprocesses and novmmemWSL— and the reason is inerrors.privateWorkingSetBytesis Task Manager's figure.autoMemoryReclaim(top level) is~/.wslconfig's setting verbatim, omitted when the file does not set it.
Exit code is 0 with the engine running or idle, 1 stopped or unreadable,
2 usage, 3 not installed.
The same object, compact, one per line, every --interval until
interrupted — newline-delimited JSON for jq, a log shipper, or a CSV:
skrog top --json --stream --interval 5s | jq -c '{t: .reading.takenAt, used: .reading.vm.memUsedBytes}'Every line stands alone, so a consumer never tracks state across lines.
While the engine is down a line carries engine and no reading. The first
reading after it has windowSecs: 0 and zero CPU: there was nothing to
average over, and the stream says so rather than inventing a figure. A
reading that failed while the engine was running sets error on that
line and the stream carries on. Exit is 0 on Ctrl-C or when the reader
goes away.
Anything that gains state reporting must gain --json in the same change and
be added here; its shape goes in cmd/skrog/jsonshapes.go with a test.