Changelog
August 8, 2026 · View on GitHub
All notable changes to this project will be documented in this file.
Format based on Keep a Changelog, and this project adheres to Semantic Versioning.
[Unreleased]
[v1.6.1] - 2026-08-08
An efficiency-scoring correction, friendlier error messages across the interactive viewer, and a pass of TUI/analysis performance work that removes redundant per-frame computation on large images.
Changed
- Efficiency analysis now counts a file that is added in one layer and deleted
in a later layer as wasted space. Deleting a file in a later layer records a
whiteout but does not reclaim the earlier layer's bytes — they remain stored
in the image and are transferred on every pull. Such files now contribute to
wastedBytes, appear in the wasted-files list (JSON export, TUI waste navigatorw), and lower the efficiency score. Scores may drop for images built with the common "download, build, thenrm" pattern; the score formula, CI rules, thresholds, and exit codes are unchanged. - The interactive file tree now caches its flatten/filter/sort result between redraws, so holding a navigation key or scrolling a large image's tree stays responsive instead of recomputing the whole tree on every frame. The cache refreshes whenever the selected layer, filter, sort, or collapse state changes. Output is unchanged; only the redundant per-frame work is removed.
- The file viewer now splits a file's contents into lines once when the file is opened instead of re-splitting the whole body on every keystroke and every frame. Scrolling and incremental search through large files (minified JSON, logs, long single-line files) no longer allocate megabytes of throwaway strings per redraw. While the viewer is open, the file-tree pipeline is no longer run for a status bar that does not use it. Rendered output and line counts are unchanged.
- The header's gradient-coloured image reference is now rendered once when the TUI starts instead of being recomputed on every frame. The image reference and theme gradient are fixed for the session, so the per-character colour interpolation was redundant work on each redraw. Output is unchanged; only the repeated per-frame computation is removed.
- Directory sizes used by the sort-by-size view are now computed once when a
layer's file tree is built instead of re-walking each directory's subtree on
every sort invocation. Toggling sort (
s) and navigating large layers with deep directory trees is now constant-time rather than O(N²) in node count. - Incremental search in the file viewer no longer allocates a temporary string
per candidate position when scanning each visible line for matches. The inner
scan now uses
strings.Indexon the lowercased line tail, reducing allocation pressure during active search through large files. - All theme-derived lipgloss styles are now built once at startup into a session-scoped style set and reused every frame. The theme is fixed for the session, so the ~300–400 per-frame style allocations that were rebuilding identical structs on every redraw are eliminated. Reduces GC pressure noticeably on low-power machines and slow terminals.
Fixed
- Running
layerx <image>with no reachable container engine (or with a Podman connection that is unconfigured, missing, or malformed) now prints the same friendly, actionable message the other commands already showed — previously the interactive command printed the raw internal error string with no recovery hint. - The archive "could not …" error in the interactive viewer no longer suggests
freeing disk space or setting
TMPDIRfor failures that are not disk-full (for example a seek or I/O error on a network mount), matching the command-line behaviour. The disk-space hint now appears only when the underlying cause is a full disk. - JSON export now reports a clean "no space left to write <path>" message on a full disk instead of leaking the internal temporary spool file path into the error shown to the user.
- Opening a file in the interactive viewer that cannot be read (for example a file that was removed by a later layer, or a daemon that became unreachable mid-session) now shows a readable message in the status bar instead of a raw internal error string.
- Saving an extracted file to a full disk now reports "not enough disk space" and names only the file you chose, instead of leaking the internal temporary file path and a raw system error into the status bar.
- A failure while fetching image size during loading is now surfaced as a brief status message instead of being silently dropped, so a size lookup that fails on its own no longer leaves the loading screen blank with no explanation.
- Every failure shown in the status bar now renders in the error colour. Previously the colour was chosen by checking whether the message began with the word "Error:", so the friendlier messages above — which no longer start with that word — would have appeared in the success colour.
[v1.6.0] - 2026-07-28
Eight built-in colour themes, transparent-background mode, and TUI visual polish across the header, file tree, and status bar.
Added
- Eight built-in colour themes selectable via
--themeflag ortheme:in.layerx.yaml:tokyo-night(default),catppuccin-mocha,kanagawa,gruvbox-dark,rose-pine,dracula,oxocarbon,cyberdream. Full reference: docs/theming.md. - All
layerx initstarter templates include a commentedtheme:block showing available values and precedence order. --themeflag now tab-completes all valid theme names in bash, zsh, fish, and PowerShell.--themeflag help and config error hints now document theme precedence (--themeflag >theme:in file > built-in default).transparent_background: truein.layerx.yamlstrips all background colours from the TUI so the terminal's own background (image, gradient, colour) shows through. Opt-in only; default behaviour is unchanged.- The image reference in the header bar now renders with a per-theme colour gradient (e.g. blue → purple on Tokyo Night, cyan → purple on Dracula) so it is visually distinct from flat panel content.
- A section divider (
├────┤) now separates the file-tree column header from the file entries, creating clear visual hierarchy within the panel.
Fixed
- Long paths in the wasted-files overlay are now mid-truncated (
/usr/local/…/numpy-1.24.dist) instead of right-truncated, so the filename is always visible. - Transient status-bar messages (
Saved:,Jumped →,Extracting…) now use the same green success colour asCopied!; error messages (Error:) use red. Previously success messages used the yellow modified colour inconsistently. theme:in.layerx.yamlwas silently ignored due torawConfigmissing the field; it now loads and validates correctly.--themeflag with an unrecognised value now exits with a clear error instead of silently falling back totokyo-night.layerx buildnow honours--themeandtheme:in.layerx.yaml; previously the theme was always tokyo-night regardless of configuration.- Syntax highlighting now uses the correct chroma style for each theme; previously
the style from the first file opened was locked in for the entire session, so
gruvbox-darkanddraculausers always saw tokyo-night syntax colours. - File viewer cursor now uses theme accent and foreground colours instead of terminal reverse-video, so it is consistent across all themes.
layerx --config PATH buildnow honours the--configflag; previously it was silently ignored and the default config was used instead.- A type error on the
theme:key in.layerx.yaml(e.g.theme: 123) now shows the targeted theme error hint instead of a generic config message. catppuccin-mocha,kanagawa, androse-pinethemes now use their native chroma syntax-highlighting styles instead ofmonokai; file viewer syntax colours now match the active theme palette.layerx initnow prints a confirmation after writing.layerx.yaml(e.g.wrote .layerx.yaml (flavour: node). Edit it to customise thresholds.).
[v1.5.3] - 2026-07-16
Configurable .layerx.yaml discovery, engine config env-var support, richer
file-tree navigation, and further hardening against crafted archives and rogue
daemons.
Added
- Page and half-page navigation in the interactive file tree and layers pane:
Ctrl-d/Ctrl-ujump half a screen down / up, andCtrl-f/Ctrl-b(alsoPgDn/PgUp) jump a full screen, complementing the existingg/Gandj/kmotions. --config PATHpersistent flag lets you point any subcommand at a specific.layerx.yamlregardless of the working directory. Useful for monorepos and scripts that invokelayerx cifrom a subdirectory.
Changed
layerxnow walks up the directory tree from the current working directory when searching for.layerx.yaml, stopping at the filesystem root. A config file at the repo root is found even when the command is invoked from a nested subdirectory. Falls back to$XDG_CONFIG_HOME/layerx/config.yaml(Linux) or%AppData%\layerx\config.yaml(Windows) when no file is found in the walk.- The Docker engine resolver now honours the
DOCKER_CONFIGenvironment variable when locatingconfig.jsonand the context metadata directory, matching the behaviour of the Docker CLI. - The Podman engine resolver now honours
PODMAN_CONNECTIONS_CONFandCONTAINERS_CONFenvironment variables before falling back to the default XDG config paths. isRegularFilePath(archive auto-detection in the resolver) now usesos.Lstatinstead ofos.Stat, so a symlink whose name looks like an image reference (e.g.nginx:latest) is not mistaken for an archive.- The cache-entry validator (
normalizeDigest) now requires exactly 64 lowercase hexadecimal characters after thesha256:prefix. This preventslayerx cache prunefrom deleting unrelated directories whenLAYERX_CACHE_DIRpoints at a shared parent directory. - When a container engine (Docker or Podman) is not reachable, the error now points at archive mode — you can inspect a saved-image archive straight from disk without any engine running. Applies to both the CLI message and the interactive error screen.
- The interactive error screen now wraps long messages to the terminal width instead of letting them overflow on one line.
- The "could not …" archive infrastructure error only suggests freeing disk
space or setting
TMPDIRwhen the underlying cause is actually a full disk; other I/O failures no longer show a misleading disk-space hint. - The
--jsonwrite confirmation now readslayerx: wrote analysis to <path>, matching thelayerx:prefix used elsewhere on stderr.
Security
- Layer analysis now caps each decompressed layer at 16 GiB, closing a
gzip-bomb path where a tiny compressed blob could expand without bound and
exhaust memory during
layerx <archive>. The extraction path already enforced this cap; the analysis path now matches it. - Archive descriptors (
manifest.json, image config, legacy root*.json) are now capped at 64 MiB and reject a tar header that overstates its size, preventing a crafted archive from forcing an unbounded allocation before any layer is read. - The image-save stream from a container engine is now capped at 64 GiB while
spooling to a temp file, so a rogue or compromised
DOCKER_HOSTcannot fill the local disk with an endless response.
Fixed
- File extraction (
xkey) no longer silently overwrites a dangling symlink at the destination path. If a broken symlink exists at the target, the filename is bumped (e.g.foo.env.1) rather than replacing the link. isDaemonUnreachableno longer fires on generic filesystem errors such as "no such file or directory" from a missing credential helper or certificate. Those strings now only trigger the daemon-unreachable path when they appear alongside a socket or named-pipe path (.sock,/pipe/), matching the actual moby SDK error format for a missing daemon socket.isImageNotFoundMessageno longer matches the bare substring "not found", which previously misclassified credential-helper and route errors as image-not-found failures.
[v1.5.2] - 2026-07-08
Multi-arch container image on GHCR, fuzz hardening, and CLI help-text polish.
Added
- Multi-arch container image published to
ghcr.io/deveshctl/layerxon every tagged release (linux/amd64 + linux/arm64). The image reuses the exact binary produced by GoReleaser, solayerx --versionmatches across brew, scoop, deb, rpm, direct-download, and the container image. Runs on Docker, Podman, or any OCI runtime. See the "Container image" section of the README for socket-mount examples and the--group-addnote for Docker on Linux.
Changed
- Extend the PR-time fuzz smoke budget from 30s to 60s per target. The 30s window occasionally exceeded its deadline on shared CI runners while a worker was still processing a slow input from the corpus, surfacing as a spurious "context deadline exceeded" failure with no crasher reproducer. The nightly fuzz workflow retains its longer budgets and is unchanged.
Fixed
- Accept all standard truthy values for the
CIenvironment variable (true,True,TRUE,1,yes), not only the exact lowercase string"true". AppVeyor setsCI=True; some Kubernetes pipelines useCI=1. Mismatches previously causedlayerx IMAGEto open the interactive TUI instead of running CI checks on those platforms. FormatSignedBytes(math.MinInt64)no longer produces a double-minus result. NegatingMinInt64overflows in two's-complement; the magnitude is now computed viauint64arithmetic, which is overflow-safe.- The file-tree renderer in the TUI now clamps the scroll offset to the current file list length before slicing, guarding against a future out-of-bounds panic if any code path shrinks the list without resetting the offset.
- Podman connection resolution now checks
CONTAINER_CONNECTIONand the active Podman connection before falling back toDOCKER_HOST. The previous order meant a legacyDOCKER_HOSTin the environment would silently shadow an explicitCONTAINER_CONNECTION=stagingoverride. - Bound the decompressed byte count walked by the per-layer tar reader in
findFileInLayer. Previously a crafted gzip stream (tiny compressed input expanding to a huge tar body) could make the tar walker consume unbounded bytes when skipping between entries — the existing per-fileMaxSaveSizecap only bounded reads of the matched file, not the walk itself. The walk is now capped atMaxLayerBlobSize(16 GiB), matching the ceiling already used when loading layer blobs from a spooled image archive. Legitimate images are unaffected; malformed inputs return a structured "reading layer tar" error instead of stalling.
[v1.5.1] - 2026-07-06
Multi-engine polish: LayerX now honours the active Docker context and Podman connection out of the box, and daemon-unreachable errors name the engine and host that actually failed.
Added
-
LayerX now honours the active Docker context and the active Podman connection, so it talks to the same daemon the engine's own CLI would. Once you've run
docker context use my-remoteorpodman system connection default staging,layerx nginx:latest(orlayerx --engine podman nginx:latest) resolves against that endpoint automatically — noDOCKER_HOST=$(podman system connection list ...)shim required.Resolution precedence, matching each engine's own CLI:
- Docker:
DOCKER_HOST→DOCKER_CONTEXT→ active context in~/.docker/config.json→ platform default socket. - Podman:
CONTAINER_HOST→DOCKER_HOST(back-compat) →CONTAINER_CONNECTION→ active default in~/.config/containers/podman-connections.json(Podman 4.4+) or~/.config/containers/containers.conf→ Linux socket probe.
Users who have never touched contexts or connections see no behaviour change: the socket-probe fallback still runs when nothing is configured. Explicit env-variable overrides still win. Archive mode (
layerx ./file.tar) is entirely unaffected — it never consults any engine config. - Docker:
-
New typed errors
engine.ErrConnectionNotFoundandengine.ErrConfigMalformedsurface a missing context/connection name or an unparseable config file with the file path and (for the not-found case) the names that do exist, instead of a generic connection-refused message.
Changed
- CLI help text (
layerx --help,layerx ci --help,layerx compare --help) and README now position the project as LayerX Image Inspector: a container image inspector for Docker, Podman, and OCI archives, rather than a Docker-only tool. Podman was already fully supported; the positioning now reflects that. No functional change to any command or flag. - README restructured for discoverability: engine-agnostic hero, feature section promoted to real H3s, added FAQ block with question-shaped headings (dive comparison, Podman support, daemonless usage, CI gating, Windows, safety, maintenance status).
--engine podmanon macOS/Windows now points the user atpodman system connection add/podman system connection defaultwhen no connection is configured, instead of asking forDOCKER_HOSTandpodman system connection list. TheDOCKER_HOST/CONTAINER_HOSTenv vars still work for scripted overrides.no container engine founderror now also mentionsdocker context useandpodman system connection defaultalongside the env-variable escape hatch.
Fixed
- Daemon-unreachable errors on
--engine podmanno longer render as "Docker daemon is not reachable. Is Docker running?" — a broken Podman connection now surfaces as "Podman connection atis not reachable" and points the user at podman system connection list/podman info.ErrDaemonNotRunninggained optionalEngineandHostfields (populated by the resolver via newWithEngineTag/WithHostTagoptions); CLI and TUI renderers switch on the engine tag so failures on--engine docker,--engine podman, and--engine auto(which now returns the picked engine name alongside the host) each get a message that reflects the engine that actually failed. Resolvers with no engine tag still fall back to the historic "Docker daemon" wording — kept for a small number of test doubles and any caller constructing the error directly.
[v1.5.0] - 2026-06-24
Multi-platform image support, an aggregated split-pane layer view in the TUI, and a supply-chain hardening pass: signed releases with SBOM and SLSA Build Level 3 provenance, OpenSSF Scorecard, and an expanded CI matrix (race, coverage, lint, govulncheck, fuzz).
Added
- Release integrity: every tagged release now ships a cosign-signed
checksums.txt(keyless, GitHub OIDC) packaged in Sigstore's bundle format (checksums.txt.sigstore.json, signature + signing certificate in one file), an SPDX SBOM per archive, and a SLSA Build Level 3 provenance attestation. Verification recipe is in the README andSECURITY.md. - OpenSSF Scorecard workflow — weekly analysis published to the public Scorecard API and uploaded as SARIF to GitHub code-scanning. Badge in the README.
- Renovate configuration (
.github/renovate.json) — monthly grouped dependency PRs withvulnerabilityAlertsopening immediately, auto-merge limited to GitHub Actions digest pin refreshes. - TUI: Split-pane view, toggled with
A. The file-tree panel splits horizontally into two synchronized halves at the same layer cursor: the top pane shows what the current layer changed (per-layer Δ); the bottom pane shows the cumulative state with provenance labels carried forward from earlier layers (a file Modified in L1 stays Modified at L7 even if L2..L7 did not touch it). Each pane has its own cursor, scroll position, and collapse state — Tab cycles layers → top → bottom → layers when the split is on, leaving the default two-state cycle untouched when it isn't. Status badge[split]advertises the mode; the bottom-pane label sits in the divider so the eye reads top→bottom as Δ → cumulative. - CI: race detector (
go test -race) on the main test job — surfaces data races inimage/(concurrent layer parsing, cache I/O) that the Windows dev environment cannot detect locally. - CI: coverage reporting via
-coverprofileuploaded to Codecov on every push and PR. - CI:
golangci-lintjob with a conservative starter set (errcheck,gosec,govet,ineffassign,staticcheck,unused). Configuration lives in.golangci.yml. - CI:
govulncheckjob that fails the build on a known vulnerability in any module the binary actually reaches. Also runs nightly vianightly.ymlso dependency advisories surface on a schedule, not only on PR. - Fuzz tests for
image/parsers —FuzzReadManifestFromSpool,FuzzScanBlobIndex,FuzzFindFileInLayer— covering the tar / archive surfaces that consume untrusted bytes. PR runs a 30-second smoke per target; a nightly workflow runs 5 minutes per target. - Golden-file test for the JSON export schema (
cmd/testdata/golden/). Locks the full byte shape of--jsonoutput (field order, indent, leaf keys) so silent reformatting can't break downstream consumers. Usego test ./cmd -updateto regenerate after an intentional schema change. --platform OS/ARCH[/VARIANT]flag for selecting a specific variant of a multi-platform image. Available onlayerx,layerx ci,layerx compare, andlayerx --json. Accepts the same shapes Docker CLI does:linux/amd64,linux/arm64,linux/arm/v7,linux/arm64/v8,windows/amd64, and the bare-arch shortcut (amd64→linux/amd64). The platform flows into the daemon's pull, save, and inspect calls, so layerx fetches and analyzes exactly the requested manifest. Without--platform, the historic behaviour is preserved (the daemon's default platform is used).- Shell completion for
--platformlists the canonical Docker / OCI variants out of the box. - Helpful error when the requested platform is not part of the image's
manifest list. The error always names what was asked for (
platform linux/ppc64le not found); when the engine exposes the multi-platform manifest list (Docker 25+ with the containerd image store), it also lists the variants the image actually carries. On the legacy image store the list is omitted rather than guessed at — a registry/manifest probe is out of scope, and listing only the locally-cached variant would misrepresent what the image really contains. - Archive mode (
layerx ./image.tar) tolerates--platformwhen it matches the archive's recorded variant and rejects it with the sameErrPlatformNotInImageshape otherwise — so a typo never silently inspects the wrong content. layerx build --platform LIST .continues to forward--platformto the engine's build (the engine governs what is built); the help text now spells out how the build-side flag relates to the top-levellayerx --platform(which selects an existing image's variant).- The TUI header now appends the active
--platformafter the image name (e.g.layerx │ nginx:latest │ linux/arm64) so multi-platform images give a visual cue which variant is on screen. layerx --jsonnow emits an optionalplatformfield alongsideimageRefwhenever--platformis set, so a downstream consumer can disambiguate two exports of the same multi-platform image without re-running layerx. JSON schema bumped to 1.0.1 (additive, backwards-compatible).
Changed
- All workflows now declare
permissions: contents: readat the workflow level; per-job write scopes are granted only where required (release, Scorecard).
Fixed
- TUI: removed duplicate
Ykeybinding in the layers panel.Ypreviously copied the highlighted layer's Dockerfile command — exactly the same job asc— so two keys were wired to the same action with no documented difference.Yis now reserved for the file viewer's "copy file content" action, matching its name and help text. Usecto copy the Dockerfile command from the layers panel. - TUI: layers-panel footer hints now surface
c copy cmdso the copy-Dockerfile-command shortcut is discoverable without opening help (?). Mirrors how the file-tree footer already advertisesy copy path. --platformmismatch on the containerd image store no longer surfaces as "image not found". The daemon's "no matching manifest for X in the manifest list entries" message contains the substringmanifest for, which the not-found classifier also matched — so a bad platform was misrouted into the image-not-found path. The platform classifier now runs first; only truly missing image references fall through to the not-found path. Affects Docker daemons running the containerd image store (where every pull goes through the daemon and there is no local short-circuit) most visibly.ArchiveResolverrewinds the archive file handle after the--platformcompatibility check, so a successful platform match no longer tripsparseLayersinto reporting "manifest.json not found" (the platform scan had advanced the file offset past the tar entries).
[v1.4.1] - 2026-06-12
layerx build thin wrapper plus a Docker-resolver fix for image-digest refs.
Added
layerx build [BUILD_ARGS...]thin wrapper arounddocker build/podman build. Forwards every argument verbatim (including-t,--build-arg,--platform,--target,--file, the build-context path, and BuildKit flags), streams the engine's native progress output to the terminal, and on success automatically opens the built image in the layerx TUI. When-t/--tagis supplied, the first tag is used as the TUI image reference (matchingdocker buildUX); the engine's--iidfileis only consulted as a fallback for untagged builds. The engine binary follows the existing--engineflag (docker/podman/auto); auto mirrors the same socket-probing order layerx already uses to pick a resolver. On build failure layerx exits with the engine's exit code and does not launch the TUI.- File viewer:
h/l(and←/→) scroll horizontally for inspecting lines wider than the panel.g/Greset horizontal scroll along with vertical, matching vim's line-jump semantics. - File viewer status bar now shows
h/lhorizontal-scroll hint alongsidej/kso the navigation controls are self-documenting. - File viewer releases mouse capture while open, allowing text to be selected
by click-dragging in the terminal. Mouse wheel scrolling is still available
via keyboard (
j/k,h/l). Mouse capture resumes when the viewer closes.
Fixed
- DockerResolver no longer tries to pull image content digests
(
sha256:<64hex>) as if they were registry references. Thereferencefilter on the Docker daemon does not match content digests, so callers that passed a raw image ID — e.g.layerx buildfalling back to--iidfilefor an untagged build — would hit "image not found" while the image was sitting in the daemon. The resolver now detects ID-shaped refs and usesImageInspectdirectly. - File viewer
h/lare now vi-style cursor moves: each keystroke advances the cursor by one column within the visible area, and the viewport only scrolls horizontally when the cursor would cross the left or right edge. Previously every keystroke shifted the entire viewport four columns at a time, which made inspecting long lines feel like tabbing through an indent and undid scroll progress onh. - File-viewer search no longer reports a "match" that is invisible. When a
search hit lies past the right edge of a long line, the viewer now
horizontally scrolls so the match is centered in view, matching vim's
sidescrollbehavior. A«marker on the left edge signals that text continues off-screen.
[v1.4.0] - 2026-06-06
Podman support and explicit cache management.
Added
layerx cache listnow shows an IMAGE column with the original image reference each entry was written with (e.g.nginx:latest,./build/app.tar). Backed by a smallmeta.jsonsidecar written next tolayers.gobon every successful cache save; entries from older versions of layerx without a sidecar render as<unknown>until they are re-cached. The sidecar is display-only —loadCacheandPruneCacheignore it. Best-effort: a sidecar write failure does not invalidate the cache.--engine docker|podman|autoflag selects which container engine to talk to.auto(the default) usesDOCKER_HOSTif set, otherwise tries the Docker socket then falls back to the Podman rootless socket on Linux.- New typed errors
ErrPodmanSocketNotSetandErrNoEngineFoundfor the--engine podmanand--engine autofailure paths. layerx cache listandlayerx cache prunesubcommands.listprints every cached digest with its size on disk and cached-at time, plus a totals footer.prune --older-than DURATION(e.g.7d,12h,2w) evicts entries older than the cutoff;prune --allempties the cache; barepruneis a dry run that lists what would be removed without touching disk and prints a hint pointing at--all/--older-thanso it isn't mistaken for an actual eviction.--dry-runpreviews any of the above.--older-thanand--allare mutually exclusive. Cache directory and override env vars (LAYERX_CACHE_DIR,LAYERX_CACHE_TTL_DAYS,LAYERX_CACHE_MAX_BYTES) are documented inlayerx cache --help. The root command's--helpCache:section also points atlayerx cache list/prune. (I-04)- Analysis cache now self-prunes by age (default 30 days) and total size
(default 1 GiB). Configurable via
LAYERX_CACHE_TTL_DAYSandLAYERX_CACHE_MAX_BYTES; set either to0to disable that limit. TTL values above 100000 days are rejected with a warn (overflow guard). Pruning runs opportunistically at the end of every successful cache write. Failures are best-effort and surface ascache prune ...warnings on stderr. (I-03) saveCachenow invokes the prune helper at the tail of every successful write, threading the analyze run's progress channel for warnings. The signature gained a trailingprogressparameter (unexported; no API change). (I-03)
Changed
layerx cache listnow displays rows newest-first (most recently cached at the top) so a freshly-written entry is visible at a glance without scrolling. The underlyingimage.ListCacheorder is unchanged (oldest-first, which is what eviction needs); only the renderer walks in reverse.isDaemonUnreachablenow classifies low-level connection errors (no such file or directory,connection refused,connect: permission denied,file does not exist) as daemon-unreachable. Previously these passed through as raw transport errors regardless of engine.cmd.selectResolveris now a swappable package-level var (test-only seam; production behaviour unchanged). Unblocks two previously skipped cancellation tests (TestRunCICheckInner_ContextCancelled, TestRunJSONExport_ContextCancelled) and adds happy-path coverage forrunInspect's CI=true and --json routes. (I-02)
Documentation
- New "Container Engines" section in README covering Docker, Podman (Linux auto, macOS/Windows manual), and archive mode.
layerx --helpreorganised: top-level Long is now ~20 lines (was ~40) with a "Common usage" synopsis listing the three primary commands (layerx IMAGE,layerx ci IMAGE,layerx compare A B) so the bare form's role is obvious. Engines prose collapsed to a one-line pointer at--engine; cache prose kept the explicitcache list/cache prunesurfacing from #f742493 in a tighter form.layerx ci --help: example.layerx.yamlblock now has aExample .layerx.yaml:header so it isn't misread as flag prose. Threshold flags (--lowest-efficiency,--highest-wasted-bytes,--highest-user-wasted-percent) now render as(default: from config (built-in 0.9))etc. instead of the misleading(default -1)sentinel.layerx compare --help: added an example for--no-cacheto surface the inherited persistent flag.layerx ci --helpandlayerx compare --helpnow end with a one-line pointer tolayerx --helpfor--engine,--json, and--no-cachedetails (the persistent flags inherited from root).
Removed
- The hidden
--refreshalias for--no-cache.--no-cachewas the documented form everywhere except the alias's own registration; the hidden flag had no discoverability path. Pass--no-cacheinstead.
Fixed
TestCopyCtx_MidStreamCancelwas racy and failed intermittently onmainonce #39 and #40 landed together: the test's blocking reader returnedio.EOFafterclose(br.release), butcopyCtxreturnsnilon EOF before its nextctx.Err()check, so the cancel could be missed. The reader now blocks on<-ctx.Done()and returnsctx.Err()instead, making the test deterministic without changing production code. Test-only.stderrProgressstop()is now idempotent. The first call closes the channel and waits for the drain goroutine; subsequent calls are no-ops. Latent — all current callers deferstopexactly once; this guards against a panic if a future refactor or test ever double-calls it. (B-08)layerx ciandlayerx --jsonnow cancel within a single 32 KiB chunk during the image-spool stage on Ctrl+C, instead of waiting for the full Docker export to land on temp disk. Closes the gap left by B-05 where pass 2 honouredctx.Done()but the initial spool copy didn't. (B-06)layerx ciandlayerx --jsonnow stream analyze progress to stderr (phase transitions plus a throttled byte/layer heartbeat) and cancel cleanly on Ctrl+C with a friendlyError: interrupted.parseLayersnow honoursctx.Done()between layer iterations. (B-05, D-03)layerx ciandlayerx --jsonnow classify "image not found", "Docker daemon not reachable", and "pull failed" as distinct, friendly one-line messages on stderr. Previously these all surfaced as raw daemon text. TUI behaviour is unchanged.- TUI now shows a clear "Could not
: . Free up disk space or set TMPDIR to a writable location and try again." message for archive infra failures (temp spool create, disk full while spooling) instead of a raw OS error string. layerx ci --helpExample header listed onlylowest-efficiency: 0.9as the default; it now also nameshighest-user-wasted-percent: 0.1, matchingconfig.Default()and what an unconfiguredlayerx ciactually enforces.- File tree size column and
ssize-sort no longer count Removed (whiteout) bytes. On layers that mostly delete files, directories full of deletions used to appear large and float to the top of the sort, pushing real live-byte hotspots down. The sort now reflects what is actually in the image at the selected layer. - Bare
layerx(no image argument) shows the usage block again. v1.3.0 silenced usage on the root command for all error paths, which also suppressed help when args were missing. - Bare
layerx ci(no image argument) now prints a one-line synopsis, the Usage line, and three concrete examples to stderr instead of just cobra's terseaccepts 1 arg(s), received 0with no actionable help. The same hint appears for too-many-args invocations. Exit code stays 2. - Malformed
.layerx.yamlerrors print a section-specific reference excerpt (rules, path-rules, version) when the failing section is known, or a general config hint otherwise — never the full command usage block.rules: nullis now rejected rather than silently zeroing thresholds. findFileInLayerno longer reports a directory as removed by its own opaque-whiteout marker (dir/.wh..wh..opqclears contents only, not the directory itself). Latent — no current user impact; tightens the contract for future API consumers.
[v1.3.0] - 2026-05-30
Compare two builds, declare path rules, and pin the config and JSON schemas. Two breaking changes — see Changed.
Added
layerx compare OLD NEWreports size, efficiency, layer, file, and waste deltas between two images side-by-side. Both arguments accept the same inputs aslayerxitself (image refs and local archives, in any mix), so you can compare a registry tag against a freshly-built tarball without pushing first.- Output ends with a one-line
verdict:summary (ok,regression reason=..., ornoop digest=...) that is easy to grep from CI logs.layerx compareexits 1 on regression (efficiency dropped or wasted bytes grew) and 2 on operational errors (daemon down, archive missing), so it drops into a build pipeline as a gate without extra wrapping. --mode compact|full|summaryand--top Ncontrol verbosity: compact (default) shows the largest deltas with a "... and N more" counter, full prints every entry, summary keeps only the header and verdict..layerx.yamlaccepts a newpath-rulessection with three rule kinds:block(fail if any layer wrote matching paths, even if a later layer deleted them),deny-waste(fail if matching paths appear in more than one layer), andmax-layer-count(fail if any one path appears in more than N layers). Globs use**for recursive matches.layerx init --flavour <node|python|java|go|generic>writes a starter.layerx.yamltuned for that stack. Path rules ship off by default — runninginitis the explicit opt-in.--jsonexport gains a top-levelschemaVersionfield (currently"1.0.0") so downstream tools can pin against the format..layerx.yamlaccepts an optionalversion: 1field at the root. Unset means 1; future schema changes will rev this.
Changed
layerx comparewith no arguments now prints a short usage hint with three concrete examples instead of an opaque "Error" line, so the command is self-explanatory on first use.layerx compareshows live progress on stderr while resolving remote images — pulling, exporting, parsing, and the resolved digest are all surfaced per side, so a slow registry pull is visibly active rather than appearing stuck. Pipe2>/dev/nullto silence; stdout stays grep-clean.layerx compareno-op message (when both inputs resolve to the same image content) is rewritten in plain language and shows both the short and full digest. The machine-parseableverdict: noop digest=...line is unchanged.layerx comparetable columns now align consistently across long paths and commands, so the LAYERS, FILE CHANGES, and WASTE CHANGES sections stay readable on real diffs.- Breaking: Esc in the main TUI no longer quits the app when nothing
is dismissable. Esc still closes the file viewer, the wasted-files
overlay, the help overlay, and clears an active filter, but mashing it
past those layers is now a no-op instead of a silent quit. Quit
remains on
qandCtrl+C. On the loading and error screens Esc continues to exit, matching the on-screen "Press q or Esc to exit" hint. - Breaking: the CI report now groups results into
Global Rules:andPath Rules:sections. Log scrapers that grep the previous flat-list format need to update their patterns. Exit codes and JSON output are unchanged.
Fixed
- Ctrl+C now cancels
layerx ci,--json, andlayerx comparewhile a slow image pull, export, or parse is in progress. Previously the process hung until the operation completed even after the interrupt was sent. - CI mode no longer silently passes when every threshold is disabled.
Setting all of
lowest-efficiency,highest-wasted-bytes, andhighest-user-wasted-percentto 0 (or omitting them all) now exits 2 with a clear message naming each flag and the config key. .layerx.yamlfiles containing akeybindings:block now load successfully. Strict mode previously rejected the entire config — rules and all — when this section was present.- A malformed
.layerx.yamlno longer drowns the parse error under the fulllayerx --helpoutput. The error line ("parsing .layerx.yaml: line 4 column 5: ...") is now the last thing printed, so the failure is visible at a glance instead of buried in 60 lines of usage text. - Efficiency scores no longer charge waste for files that were deleted
between layers and a different file with the same path was added later
(
apt-get install→apt-get clean→ reinstall). Each delete-then-readd cycle is now treated as a separate run, so only true duplicates within a single run count toward wasted bytes. wasted %rule output now reads as a percent. The rule used to print the raw fraction (e.g.0.10) labelled as a percent; it now renders as10.0%in both Actual and Threshold columns. Theefficiencyrule matches the same format (92.5%) so all three rules agree.layerx compare ./app.tar ./app.taris now recognized as a no-op immediately, even when the resolver cannot return a content digest. The short-circuit emitsverdict: noop reason=path-equalso the verdict line stays well-formed for parsers that scan fordigest=orreason=.--topis now ignored in--mode summaryand--mode fullinstead of rejecting--top 0with an error. Compact mode keeps the existing range check.- Filenames with CJK or emoji characters no longer overflow the selected filetree row. Padding now measures display width to match the unselected branch.
- Multi-GB layers (ML model images, dataset bundles) no longer load the entire compressed blob into memory before parsing. Peak memory during analysis stays bounded by the largest single layer's parsed file tree rather than the largest compressed layer.
- Long Dockerfile commands wrap on the nearest space within the panel width, eliminating an off-by-one that could force a mid-word cut on odd-width panels.
- File extraction (
xsave, content viewer) on multi-GB images no longer loads every layer blob into memory at once. The per-extract memory ceiling is now one layer blob — the one currently being scanned — so ML / dataset images that previously OOMed the process now work, and TUI key-mash sequences that fired concurrent extracts are bounded. xsave now writes via temp-file + rename instead of in-place. A process kill (Ctrl+C, OOM, power loss) mid-write no longer leaves a truncated file at the user's chosen path; the target either holds the complete pre-write content or the complete new content. The save resolves symlinks before writing (so saving over a symlink updates the link's target, not the link itself) and applies the process umask to the chosen mode bits, matching the prioros.WriteFilesemantics.- Opening a large source file in the viewer no longer freezes the TUI while syntax highlighting runs. The Chroma tokenize/format pass moved off the input goroutine; the file shows immediately in plain text and the colored version swaps in when ready.
- Filenames with CJK or wide-emoji characters no longer overflow the Waste panel's path column. Truncation now measures display columns rather than rune count, keeping the surrounding table aligned.
- Layer blob loading is now bounded by an explicit per-blob size cap. A malformed image archive that declares a single layer is petabytes no longer causes runaway allocation.
- Image pulls now fail loudly when the registry rejects the request.
Authentication failures, "manifest not found", and registry 5xx
responses arrive from the daemon as in-band JSON
errorDetailevents on a 200 stream; previously both the progress and non-progress paths swallowed those events and reported the pull as successful, then surfaced as a confusing "manifest.json not found" during export. The error message from the registry is now returned as the pull failure cause. - An empty, whitespace-only, or comments-only
.layerx.yamlno longer blocks startup. The M12 contract treats such files identically to a missing config — fall back to defaults — and a stub placeholder file in a repository will load cleanly. - Replacing a regular file with a hardlink at the same path is now charged as wasted bytes. The original file's bytes still ship in the earlier layer's tar even though the live filesystem now points elsewhere, so the prior contents are dead weight in the image. The efficiency walk previously skipped the replacement node entirely and the prior occurrence dropped out of the waste total.
[v1.2.3] - 2026-05-28
Added
- Inspect local image archives without a Docker daemon: pass a path to a
docker savetarball or OCI layout tarball (e.g.layerx ./build/app.tar) and LayerX reads the file directly. Works for the TUI,--jsonexport, andlayerx ci. Useful in CI runners and air-gapped environments where the image is already on disk and starting a daemon is unwanted. - Auto-detection: any argument that resolves to an existing regular file
is opened as an archive; everything else still goes through the Docker
daemon. No new flags; existing
layerx nginx:latestworkflows are unchanged. - File content viewer and save-to-disk (
x) work in archive mode too — full feature parity with daemon-backed inspection.
Changed
- Manually-built binaries (
go buildfrom source) now include the git commit and build date in--versionoutput, picked up automatically from the metadata Go embeds at build time. Release builds and source-tarball checkouts without git history are unchanged.
Fixed
- Clearer errors for archive mode: a missing path reports "Archive not found" instead of a generic Docker error; a malformed tarball reports "Not a valid image archive" instead of a low-level parse error. A permission-denied archive now reports "Permission denied" instead of being mislabelled as "not found".
- Saving or viewing a file from a layer enforces the same 2 GiB cap as the daemon-backed copy path. A crafted tar entry with an inflated size header can no longer make LayerX exhaust memory.
- Archive image-size readout (shown during loading) now sums only the
layer blobs declared in the manifest, not unrelated tar entries like
manifest.jsonand the config blob.
[v1.2.2] - 2026-05-26
A correctness and reliability release across the TUI, CI mode, config, and large-image handling.
Added
- Background saves with auto-rename — slow disks no longer freeze the TUI,
and existing files get
name.1,name.2, … instead of being overwritten.
Changed
- Breaking — CI:
layerx ci/CI=true layerxexit1only on rule failure; internal errors (daemon down, bad config, JSON write failure) exit2. Threshold flags live only onlayerx ci;.layerx.yamlis decoded strictly; out-of-range /NaN/±Infthresholds error up front; threshold0or negative disables that rule (previously failed every image).
Fixed
- TUI:
q/Ctrl+Cexits within ~1 s during pulls, extraction, and saves;qtypes normally inside the filter and viewer search. CRLF/CR endings, CJK and wide characters, narrow-terminal pull totals, and trailing\nno longer smash panel borders. Waste overlay shows accurate counts, handles unknown intro layers, excludes whiteouts, and preserves filter state on failed jumps. - Analyzer: file viewer and extraction read from the selected layer (not the final image); non-regular files return a clear error. Strict whiteout handling for re-add and reserved prefixes; directory↔file type flips across layers; hardlinks count toward efficiency. Search handles non-ASCII case changes; filter input capped at 256 chars.
- Scale: 8 GB images no longer peak at 8 GB RAM (layer parsing spools to disk); 100 GB tar entries cap at 2 GiB instead of allocating upfront. Efficiency report and JSON export are deterministic across runs.
- Plumbing:
LAYERX_CACHE_DIR=~/…expands~; transient I/O doesn’t evict good cache files; concurrent--jsonruns use unique temp names; no.tmp-*orphans after crashes; multi-arch pulls don’t deadlock. Shell completion under Git Bash / WSL strips CRLF; hung daemon times out after 1 s.--versionno longer prints(commit none, built unknown)for binaries built without-ldflags.
[v1.2.1] - 2026-05-24
Correctness fixes for layer stacking and the file viewer.
Fixed
- Directory metadata changes (mode, UID, GID) introduced by a later layer
(e.g.
RUN chmod 0777 /app) are now correctly attributed to that layer instead of being silently dropped from the stacked tree. - Pressing
Enter(view) orx(save) now extracts the file as it exists at the selected layer, not the final image. Previously, viewing/etc/configat layer 2 always showed layer 5's contents. - The pull-progress line stops being truncated on large images. The
loading-panel width adapts so the bytes total (
4.7 GB) is never clipped. - Waste overlay navigation: pressing
jpast the collapsed top-20 list now auto-expands and continues scrolling.Gjumps to the true last row, expanding when needed. Manualatoggle is unchanged. - The waste overlay panel title now reads
Wasted Files 14/30, matching theLayers 14/30andFile Tree 14/30convention. The body header is simplified to5.6 MB wasted across 31 files.
[v1.2.0] - 2026-05-23
Per-image-digest analysis cache — repeat runs against an unchanged image feel instant.
Added
- Analysis cache. Parsed layer trees are written to
${LAYERX_CACHE_DIR or os.UserCacheDir()/layerx}/<digest>/layers.goband re-used on the next run. A cache hit skips the DockerImageSaveand tar parse entirely. --no-cache(alias--refresh) on the root command andlayerx ci. Bypasses the cache for the current run; the run still writes the cache on success.- The TUI briefly shows "loaded from cache" on hit, and surfaces non-fatal cache I/O failures as a transient status message rather than silently swallowing them.
Fixed
- Typing
qinside the file-tree filter or viewer search no longer quits the TUI. Filter queries containingq(e.g.jquery,graphql) now type normally;Ctrl+Ccontinues to quit unconditionally. - A concurrent
docker pullmid-run that retags the image no longer poisons the cache — the digest is re-checked after resolve. - Empty or unsafe digests are rejected before they reach the cache key, so a misbehaving Resolver can't write to an arbitrary cache slot.
- Transient I/O during cache decode (permission flip, EBUSY, etc.) no longer evicts an otherwise-valid cache file.
- Cache writes survive power loss between rename and writeback — the temp file is fsynced before the atomic rename.
- The TUI no longer flashes "Pulling …" before any actual progress event; cache hits jump straight to "loaded from cache".
- Bad
LAYERX_CACHE_DIRoverrides print one stderr warning and fall back to the OS default cache dir.
Changed
- Release artifact filenames dropped the version segment:
layerx_linux_amd64.deb(waslayerx_1.1.0_linux_amd64.deb). - README install snippets use
/releases/latest/download/<name>so they auto-track the latest release. Older releases keep their versioned names.
[v1.1.0] - 2026-05-21
Layer net-delta column and the Waste Navigator.
Added
- Waste Navigator (
w). Centred overlay listing the worst duplicate files by total wasted bytes. Columns: path | wasted bytes |xN(layer count) |L<n>(introducing layer).Enterjumps to the introducing layer with the cursor on the file.atoggles between the top 20 and the full list (capped at 500).ycopies the highlighted path.Esccloses;q/Ctrl+Cquit. Empty state when efficiency is 100%. - Layer net-delta column. The layers panel now shows per-layer change
in live filesystem size — green when negative (cleanup layers), accent
when ≥10% of final size, dim otherwise.
Scycles Change → Stored → both. The status bar surfaces the same number for the selected layer. - JSON export: each layer entry gains a
netDeltafield. - Help overlay reorganised: dedicated Layers section, multi-column layout on wide terminals, footnote explaining Change vs Stored.
- Clipboard (OSC 52).
ycopies the focused file path;Ycopies file content (in the viewer) or the layer command (in the layers panel). Works inside tmux/SSH sessions. - Search in the file viewer.
/opens search;n/Nnavigate matches; the current match is rendered with a distinct style. - Layer-origin annotations. The file tree shows
(LN)for files introduced in a different layer. The viewer title surfaces the introducing layer and the Dockerfile command.
Changed
- Filter Enter is a two-step: first Enter confirms and closes the input, second Enter opens the selected file.
- Backspace on an empty filter input clears + dismisses (Esc still works).
Esccascade: viewer search → close viewer search → close viewer → close filter → close help → quit.
[v1.0.0] - 2026-05-18
Initial public release. Interactive TUI for inspecting Docker image layers.
Added
- Layer browser with diff colouring (green Added, yellow Modified, red Removed, grey Unchanged) and per-layer file trees.
- File tree with whiteout-aware layer stacking — handles both
.wh.<name>and.wh..wh..opqopaque whiteouts correctly. - File content viewer (
Enter) — the headline feature dive has never shipped. Scrollable text with line numbers, binary detection, and a 1 MB truncation notice for large files.Escreturns to the tree. - File extraction to disk (
x) — saves the focused file to the current working directory at full size (no 1 MB cap). - Filter, diff-only, and sort.
/opens substring filter;dhides unchanged files;scycles default → largest first → smallest first. All three compose. Filter and diff persist across layer switches. - Efficiency score and wasted bytes, computed by detecting files that appear in more than one layer (all but the first occurrence is waste).
- CI mode —
layerx ci <image>(orCI=true layerx <image>) evaluates against three configurable rules:lowest-efficiency,highest-wasted-bytes,highest-user-wasted-percent. Exits 1 on failure, 0 on pass, with a human-readable report. .layerx.yamlfor CI thresholds. Missing config silently uses defaults; CLI flags override config.- JSON export —
--json <path>skips the TUI and writes a flat schema with image metadata, layers, files, diff types, and efficiency data. Schema is documented and tested for round-trip stability. - Shell completion —
layerx completion [bash|zsh|fish|powershell]. Custom completer suggests local Docker images for the image argument. - Help overlay (
?) with a key reference for every panel. - README and MIT LICENSE.
Technical
- Single static binary, no runtime dependencies beyond a running Docker daemon.
- Built on
github.com/moby/moby/client(the actively maintained Docker SDK) with API-version negotiation. - TUI on bubbletea v2 / lipgloss v2 / bubbles v2.
- Cross-compiled for
linux/{amd64,arm64},darwin/{amd64,arm64},windows/{amd64,arm64}..deband.rpmartifacts for Linux.