Jesse A. Green / notebook

dive Admin & Developer Guide

Generated June 2026 from wagoodman/dive at d6c6919

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

This guide is for operators, packagers, security reviewers, and developers who need to understand dive beyond surface usage: how it parses container images, how the file-tree comparison and efficiency scoring work, every configuration field and its precedence, the runtime and security posture, and where to extend the codebase. If you only want to install and use dive, start with the User Guide and come back here when you need the internals.


1. Executive Summary

Aspect Detail
Project wagoodman/dive
Purpose Explore Docker/OCI image layers and quantify wasted space
Language Go (module declares go 1.24)
License MIT
Binary Single static executable, cmd/dive
CLI framework spf13/cobra via Anchore's clio configuration framework (anchore/clio, anchore/fangs, spf13/viper)
TUI framework awesome-gocui/gocui
Image sources Docker engine, Podman engine (Linux/macOS), Docker/OCI tar archive
Runtime services None — no network listener, no daemon, no persistent state
Distribution GoReleaser → deb/rpm, Homebrew, Arch, Nix, Choco/Scoop/winget, Docker images on docker.io and ghcr.io (amd64/arm64)
Privilege surface Requires access to a container engine socket (effectively root-equivalent)

dive is a read-mostly developer tool. The "product" is the efficiency analysis of an image's layers: it stacks each layer's tar contents into a virtual filesystem, diffs adjacent stacks, and computes how many bytes are wasted by files that are duplicated, moved across layers, or deleted in later layers. It exposes that analysis interactively (a gocui TUI) and headlessly (a CI gate with a pass/fail exit code).

Note

There is no server to operate. "Operations" here means packaging the binary, wiring it into CI, and configuring engine access — not running a service.


2. Architecture

2.1 High-level flow

                          dive <image>  /  dive build ... / CI=true dive <image>
                                         │
   cmd/dive/main.go ── clio.Application ─┤  (anchore/clio: cobra + viper config)
                                         │
   cmd/dive/cli/cli.go  create()         │  builds root cmd + subcommands:
       ├── command.Root(app)             │     dive [IMAGE]      (fetch + analyze)
       ├── command.Build(app)            │     dive build ...    (docker build + analyze)
       ├── clio.VersionCommand           │     dive version
       └── clio.ConfigCommand            │     dive config
                                         │
   internal/command/{root,build}.go      │  resolve options.Application, pick UI
                                         ▼
   dive.GetImageResolver(source) ──► image.Resolver
       ├── docker.NewResolverFromEngine()    (Docker daemon)
       ├── docker.NewResolverFromArchive()   (tar on disk)
       └── podman.NewResolverFromEngine()    (Podman, linux/darwin)
                                         │
   adapter.ImageResolver(r).Fetch/Build  │  observer wrappers emit progress events
                                         ▼
   image.Image{ Trees []*filetree.FileTree, Layers []*image.Layer }
                                         │
   adapter.NewAnalyzer().Analyze ──► image.Analyze()
       └── filetree.Efficiency(trees) ─► (score, EfficiencySlice)
                                         │
                 ┌───────────────────────┼───────────────────────┐
                 ▼                        ▼                       ▼
   bus.ExploreAnalysis()        adapter.Evaluator (CI)    adapter.Exporter (--json)
   → gocui TUI (ui/v1)          → ci rules pass/fail      → JSON file

All cross-component progress ("Loading image", "Analyzing image", "Evaluating image", "Exporting details") flows over an in-process event bus (wagoodman/go-partybus) rather than direct calls, so the UI and the analysis pipeline stay decoupled.

2.2 Package map

Package (path) Responsibility
cmd/dive main() — wires build-time identification into the clio app
cmd/dive/cli Builds the clio Application and assembles the cobra command tree
cmd/dive/cli/internal/command root.go (analyze), build.go (build+analyze) command definitions and the shared run() pipeline
cmd/dive/cli/internal/command/adapter Observer wrappers around resolve/analyze/evaluate/export that publish progress events
cmd/dive/cli/internal/command/ci CI rule definitions, evaluator, and report rendering
cmd/dive/cli/internal/command/export JSON export model (Export, Layer, Image, FileReference)
cmd/dive/cli/internal/options All configuration structs: Analysis, CI, CIRules, Export, UI* — the source of truth for flags and config-file keys
cmd/dive/cli/internal/ui + ui/v1/** The gocui TUI: app loop, controller, layout manager, views, viewmodels, keybindings
dive GetImageResolver, ImageSource enum, source:// scheme parsing
dive/image Image, Layer, Analysis, Resolver/ContentReader interfaces, Analyze()
dive/image/docker Docker engine resolver, archive resolver, OCI/Docker tar parsing, build wrapper
dive/image/podman Podman engine resolver and build wrapper (Linux/macOS only)
dive/filetree The core data model: FileTree, FileNode, FileInfo, diff/compare, layer stacking, and the Efficiency() algorithm
internal/bus partybus event bus singleton + helpers (StartTask, Report, Notify, ExploreAnalysis)
internal/bus/event Event type constants and payload parsers
internal/log Logging singleton (Anchore go-logger)
internal/utils Small helpers (CleanArgs, IsNewView, formatting)

2.3 Command construction (clio, not plain cobra)

dive does not register subcommands the typical Cobra way (an init() tree of AddCommand calls). Instead cmd/dive/cli/cli.go builds an anchore/clio application:

clioCfg := clio.NewSetupConfig(id).
    WithGlobalConfigFlag().   // persistent -c <path> to load an application config
    WithGlobalLoggingFlags(). // persistent -v / -q tied to the logging config
    WithConfigInRootHelp().   // --help renders the full resolved config
    WithUI(ui.None()).
    WithInitializers(/* wire bus + logger into clio state */)

app := clio.New(*clioCfg)
rootCmd := command.Root(app)
rootCmd.AddCommand(
    clio.VersionCommand(id),
    clio.ConfigCommand(app, nil),
    command.Build(app),
)

The practical consequence for maintainers and for documentation tooling: flags and config keys are declared on option structs, not on the commands. Each struct in internal/options implements one or more clio interfaces — clio.FlagAdder (AddFlags), clio.FieldDescriber (DescribeFields), clio.PostLoader (PostLoad). clio merges defaults → config file → environment → flags, then calls PostLoad() for validation and derived values.


3. Requirements

Component Requirement Notes
OS Linux, macOS, or Windows Podman source is gated to Linux/macOS via build tags (//go:build linux || darwin)
Container engine Docker or Podman Default is Docker; engine selected by container-engine / --source
Engine socket access Read access to the daemon socket Default /var/run/docker.sock; overridable via DOCKER_HOST/DOCKER_CONTEXT
Client binary on PATH docker (or podman) for dive build build.go shells out via exec.Command
Terminal A real TTY for interactive mode Not required for --ci / CI=true
Build toolchain (from source) Go matching the module's go 1.24; CGO not required (CGO_ENABLED=0) go install works but omits version stamping

Engine connection environment

The Docker engine resolver honors the standard Docker client environment (read in dive/image/docker/engine_resolver.go):

Variable Purpose
DOCKER_HOST Daemon endpoint (TCP or unix socket)
DOCKER_CONTEXT Named Docker context to resolve the host from
DOCKER_TLS_VERIFY Enable TLS verification of the daemon
DOCKER_API_VERSION Pin the negotiated API version (client/daemon mismatch)

4. Configuration Reference

4.1 Resolution order and locations

Configuration is resolved by clio/viper from (lowest to highest precedence): built-in defaults → config file → environment → command-line flags. After binding, each option struct's PostLoad() runs to validate and compute derived state.

Config files are auto-discovered (.yaml or .yml) at:

  • $XDG_CONFIG_HOME/dive/*.yaml
  • $XDG_CONFIG_DIRS/dive/*.yaml
  • ~/.config/dive/*.yaml
  • ~/.dive.yaml

Explicit selection: -c/--config <path> or DIVE_CONFIG=<path>. Inspect the merged result with dive config.

4.2 Analysis options (options/analysis.go)

YAML key Flag Default Validation (PostLoad)
container-engine --source docker Must be one of docker, podman, docker-archive; otherwise a warning is logged and it resets to docker
ignore-errors -i, --ignore-errors false —

Important

--source binds to the same field as container-engine. The image source is resolved in PostLoad(): if the image argument carries a scheme:// prefix (DeriveImageSource), that wins; otherwise the source is parsed from container-engine/--source (ParseImageSource). docker-tar is accepted as an alias for docker-archive.

4.3 CI options (options/ci.go, options/ci_rules.go)

YAML key Flag Default Notes
ci --ci false Also enabled when env CI is true/1/yes
ci-config --ci-config .dive-ci If the file exists, its rules: block is loaded over the defaults
rules.lowest-efficiency --lowestEfficiency 0.9 Ratio 0–1, or disabled/off/false/empty
rules.highest-wasted-bytes --highestWastedBytes disabled Human size (B,kB,MB,GB), or disabled
rules.highest-user-wasted-percent --highestUserWastedPercent 0.1 Ratio 0–1, or disabled

Legacy camelCase keys (lowestEfficiency, highestWastedBytes, highestUserWastedPercent) are still accepted and mapped onto the snake-case fields, with a deprecation warning logged in PostLoad().

Note

The built-in CI defaults (0.9 / disabled / 0.1) differ from the illustrative values shown in the project README (0.95 / 20MB / 0.20). The README values are an example .dive-ci, not the defaults. When no .dive-ci is present, the built-in defaults above apply.

4.4 Export options (options/export.go)

YAML key Flag Default Notes
json-path -j, --json (empty = disabled) Writes the analysis JSON; PostLoad errors if the target directory does not exist. Setting this skips the TUI.

4.5 UI options (options/ui*.go)

YAML key Default Validation
diff.hide (none) Each entry must be added/removed/modified/unmodified; invalid entries warn and are ignored
filetree.collapse-dir false —
filetree.pane-width 0.5 Must be >0 and <1; otherwise warns and resets to 0.5
filetree.show-attributes true —
layer.show-aggregated-changes false —
keybinding.* see below Comma-separated multi-key; first key is the UI hint

Keybinding fields (options/ui_keybindings.go) are grouped:

  • Global: quit, toggle-view, filter-files, close-filter-files
  • Navigation: up, down, left, right, page-up, page-down
  • Layer: compare-all, compare-layer
  • Filetree: toggle-collapse-dir, toggle-collapse-all-dir, toggle-added-files, toggle-removed-files, toggle-modified-files, toggle-unmodified-files, toggle-filetree-attributes, toggle-sort-order, toggle-wrap-tree, extract-file

Note

The keybinding model uses reflection (getUIBindingValues / createKeyBindings) to convert between the YAML-friendly UIKeybindings struct and the runtime key.Bindings. Adding a binding means adding a field with matching yaml/mapstructure tags and registering a default in key.DefaultBindings().


5. The Analysis Engine (core internals)

5.1 Image ingestion

Each image is normalized into image.Image{ Trees []*filetree.FileTree, Layers []*image.Layer }. For the archive path (dive/image/docker/image_archive.go), NewImageArchive reads the tar in one pass:

  • Layer blobs are detected by extension (.tar, .tar.gz/.tgz) for the legacy Docker format, and by content sniffing for the OCI format (Docker 25+) — entries under blobs/ are tried as gzip/estargz, then zstd, then plain tar, then JSON. This is how dive supports uncompressed, gzip, estargz, and zstd layer compression.
  • manifest.json drives layer ordering when present; for pure OCI images (no manifest.json) the config blob is located via isConfig() and a synthetic manifest is built.
  • Each layer tar becomes a filetree.FileTree via processLayerTar → AddPath for every entry; missing config/manifest yields could not find image config / could not find image manifest.

ToImage() walks history entries (skipping EmptyLayer history items) to attach the originating CreatedBy command to each content-bearing layer.

5.2 The file tree model (dive/filetree)

  • FileInfo holds tar metadata plus an xxhash content hash (getHashFromReader). Compare() returns Unmodified only when type, hash, mode, UID, and GID all match — otherwise Modified.
  • FileNode is a tree node with lazily computed size, children map, and a memoized slash path. Whiteouts use the OverlayFS convention: .wh.<name> marks a removed file; .wh..wh..opq marks an opaque directory and is rejected at add time.
  • DiffType is Unmodified | Modified | Added | Removed; merge() collapses differing child diffs into Modified so a directory's diff reflects its subtree.
  • Stacking (StackTreeRange, Stack) overlays an upper tree onto a copy of a lower tree: whiteouts remove paths; everything else is added. This produces the "filesystem as of layer N" view.
  • Comparison (CompareAndMark) marks each node in the lower tree with its DiffType relative to the upper layer, visiting leaves-first so parent diffs derive correctly from children.

5.3 Layer comparison modes (comparer.go)

The Comparer caches stacked/diffed trees keyed by a four-index TreeIndexKey and offers two index strategies:

  • Natural indexes — the bottom tree grows as you descend layers while the top tree is the single selected layer. This backs the default "what changed in this layer" view (compare-layer, Ctrl+L).
  • Aggregated indexes — the bottom is fixed at the base and the top spans all layers up to the selection, backing the "aggregated changes" view (compare-all, Ctrl+A).

5.4 Efficiency scoring (efficiency.go)

The headline metric. For every leaf path across all layers, Efficiency() records each occurrence's size, tracking the cumulative size (sum across layers) and the minimum discovered size. Whiteout entries contribute the size of the previously stacked file/dir they remove.

score = Σ(minDiscoveredSize over all paths) / Σ(cumulativeSize over all paths)
  • A path seen once contributes equally to both sums → no penalty.
  • A path seen in ≥2 layers (duplicated/moved) or removed via whiteout adds to the cumulative sum but not proportionally to the minimum → score drops. Such paths are recorded as EfficiencyData and surfaced as the inefficient files list (sorted by cumulative size).
  • WastedBytes = Σ cumulative size of inefficient files. WastedUserPercent = WastedBytes / userSizeBytes, where userSizeBytes excludes the base (index 0) layer.
  • Empty/scratch images score 1.0.

Note

The score is explicitly described in-code as an estimate/heuristic. Issue #684 notes a double-counting edge case in the wasted-space report; treat the number as a strong signal, not an exact accounting.

5.5 Output paths

After Analyze(), the shared run() (in root.go) branches:

  1. --json set → adapter.NewExporter(afero.NewOsFs()).ExportTo(...) writes the export.Export model and returns.
  2. CI enabled → adapter.NewEvaluator(rules).Evaluate(...); non-pass returns an error (non-zero exit).
  3. Otherwise → bus.ExploreAnalysis(*analysis, content) hands off to the gocui TUI.

5.6 CI evaluation (command/ci)

Each rule implements Rule{ Key(), Configuration(), Evaluate(*image.Analysis) (RuleStatus, string) }:

  • LowestEfficiencyRule fails when threshold > analysis.Efficiency.
  • HighestWastedBytesRule fails when analysis.WastedBytes > threshold (threshold parsed via humanize.ParseBytes).
  • HighestUserWastedPercentRule fails when analysis.WastedUserPercent > threshold.

Disabled values ("", disabled, off, false) produce a DisabledRule that always reports SKIP. Rule constructors validate ranges (ratios must be 0–1) and return errors for malformed config — surfaced as a misconfiguration. The evaluator renders a three-section report (Analysis, Inefficient Files table, Evaluation) with a PASS/FAIL [pass:n fail:n warn:n skip:n] summary and sets the process exit accordingly.


6. The TUI (gocui)

cmd/dive/cli/internal/ui/v1 implements the interactive interface on awesome-gocui/gocui:

  • app/app.go Run() is the entry point. It sleeps ~100ms before gocui.NewGui to dodge a documented termbox init race when dive is the first process in a container (issue #513 territory), then enters MainLoop.
  • app/controller.go coordinates view updates/renders and pane switching, emitting wrapped errors (controller failed update, controller unable to switch to next pane, etc.).
  • layout/manager.go + layout/compound arrange the footer (status, filter) and the column views (the compound layer/layer-details/image-details column, plus the file tree). The compound layout constrains real-estate below ~90 columns.
  • view/* are the individual panes (layer, filetree, layer_details, image_details, filter, status, debug); viewmodel/* hold their state (layer selection, layer-set state, filetree view model).
  • key/binding.go binds configured keys to actions and renders the help hints; the first configured key per action is shown.

The TUI is intentionally isolated behind the event bus and the ui.UI interface (ui.None() is used for non-interactive runs), so analysis logic never depends on gocui.


7. Security Considerations

Caution

Engine socket access is the dominant risk. dive's default mode talks to the Docker/Podman daemon. Granting socket access (or running the container with -v /var/run/docker.sock:/var/run/docker.sock) is effectively granting root-equivalent control of the host. Treat any environment where dive runs against a live daemon as privileged.

Area Posture
Network None. dive opens no listening port and runs no server.
Authentication / secrets None handled by dive itself. Registry pulls/auth are delegated to the underlying engine.
Subprocess execution dive build and the podman/docker build paths run exec.Command("docker"/"podman", ...) with DisableFlagParsing: true, so all build args pass straight through to the engine. Do not feed untrusted strings into dive build.
Filesystem writes --json writes one file (directory must pre-exist). The TUI "extract file" action writes extracted file contents to disk — review issues #620/#687 (extraction crashes) before relying on it on untrusted images.
Untrusted images Analysis reads attacker-controllable tar archives. Malformed archives are reported as errors (or skipped with --ignore-errors); content sniffing is bounded by a small buffer. There is no sandboxing of analyzed content beyond Go's tar reader.
Supply chain Dependencies are license-gated at build time via bouncer (.bouncer.yaml permits BSD/MIT/Apache/MPL/ISC/WTFPL/Unlicense). Issue #645 tracks CVEs surfacing in published images — keep base images current.

Hardening guidance:

  • In CI, prefer --source docker-archive <tar> so dive never needs the live daemon socket.
  • If the socket is required, scope the runner's daemon access and avoid mounting the socket into long-lived or multi-tenant containers.
  • Pin the dive image by digest and rebuild regularly to absorb base-image CVE fixes.

8. Operations & Packaging

8.1 There is no service to run

dive is a CLI invoked on demand. "Deployment" means distributing the binary and configuring engine access. There is no start/stop, no health endpoint, no logs to rotate beyond the optional log.path file (log.enabled, log.level).

8.2 Release pipeline (.goreleaser.yaml, Taskfile.yaml, .binny.yaml)

  • GoReleaser builds static binaries (CGO_ENABLED=0, -extldflags '-static') for windows/darwin/linux × amd64/arm64/ppc64le, stamping main.version, main.gitCommit, main.buildDate, main.gitDescription via ldflags.
  • Artifacts: tar.gz (zip on Windows), deb + rpm (nfpms), a Homebrew tap (wagoodman/homebrew-dive), and multi-arch Docker manifests on both docker.io/wagoodman/dive and ghcr.io/wagoodman/dive.
  • Toolchain is pinned by binny (.binny.yaml): golangci-lint, goreleaser, chronicle (changelog), glow, bouncer, task, gh.
  • Task targets (Taskfile.yaml): task (validations = static-analysis + test), task lint, task unit, task cli, task build/task snapshot (goreleaser snapshot), task changelog, task release.

8.3 Tests

  • Unit tests cover the filetree model and efficiency math (filetree/*_test.go), CI rules/evaluator (command/ci/*_test.go), and export (command/export/*_test.go); coverage threshold is enforced at 25% (Taskfile.yaml).
  • CLI/snapshot tests (cmd/dive/cli/cli_*_test.go) drive the assembled root command with go-snaps golden files, exercising build, CI gate, JSON output, config loading, and image loading from docker/podman/archive sources. They use DIVE_CONFIG to control config and a captured stdout/stderr harness.
  • Acceptance tests (ci-test-* tasks) install the deb/rpm and run the docker image against a known test image across compression algorithms (uncompressed/gzip/estargz/zstd).

9. Extending dive

Goal Where to work
Add a new image source Add an ImageSource constant + String()/Parse/Derive cases in dive/get_image_resolver.go, implement image.Resolver (and ContentReader) in a new dive/image/<src> package, and wire it into GetImageResolver. Issue #406 requests a go-containerregistry resolver as exactly this kind of extension.
Add a CI rule Implement ci.Rule in command/ci/rules.go, register it in Rules(...), and add the corresponding field + tags in options/ci_rules.go.
Add a config option Add the field with yaml/mapstructure tags to the relevant options/* struct, set its default in the Default*() constructor, describe it in DescribeFields, and validate in PostLoad. Flags go in AddFlags.
Add a TUI keybinding/action Add the field in options/ui_keybindings.go, a default in key.DefaultBindings(), and the BindingInfo/action in the relevant view or app.go.
Change the efficiency heuristic dive/filetree/efficiency.go (Efficiency) with tests in efficiency_test.go.

Tip

Because flags and keys are reflection/interface-driven on the option structs, the surest way to enumerate the current CLI/config surface is to read cmd/dive/cli/internal/options/*.go — not the command files. A purely text/AST scan of the cobra commands will under-report, because the commands declare almost no flags directly.


10. Known Issues & Community Signals

Drawn from the project's open issues — useful for triage and roadmap context:

Issue Theme
#546 Snap install conflicts with apt-installed Docker; missing interface connections
#649 Wrong default Docker host detected on Windows
#605 Diving fails on Windows 10 with Rancher Desktop
#513 Hang immediately after the main view opens (TUI init race)
#452 could not find image config on certain images/builders
#681 Severe slowdown when many layers modify the same files (comparison cost)
#684 Double-counting in the potential-space-savings report
#620 / #687 Crash / failure when extracting a file from a layer
#647 Image Details window scroll not working
#645 High CVEs surfaced in a published build
#406 / #695 / #680 Feature requests: go-containerregistry source, color theme/config in .dive.yaml

Architectural notes for maintainers:

  • The single-image-argument constraint is enforced in root.go (exactly one argument is required); multi-image comparison is not supported.
  • Layer comparison builds and caches stacked trees per index; the #681 slowdown is inherent to repeatedly stacking/diffing large identical subtrees and would need algorithmic work in comparer.go/file_tree.go.
  • --source overloading container-engine is a known ergonomic wrinkle (the flag name and the config key differ); preserve the PostLoad derivation if refactoring.

11. Quick Reference

Commands: dive [IMAGE] · dive build [docker build args] · dive version · dive config

Key flags: --source {docker|docker-archive|podman} · -i/--ignore-errors · --ci · --ci-config <path> · -j/--json <file> · -c/--config <path> · -v / -q

Image source forms: dive <image> --source <src> ≡ dive <src>://<image> (sources: docker, docker-archive/docker-tar, podman)

Config search: $XDG_CONFIG_HOME/dive/*.yaml → $XDG_CONFIG_DIRS/dive/*.yaml → ~/.config/dive/*.yaml → ~/.dive.yaml (or -c / DIVE_CONFIG)

CI rule defaults: lowest-efficiency: 0.9 · highest-wasted-bytes: disabled · highest-user-wasted-percent: 0.1 (default rules file path .dive-ci)

Engine env: DOCKER_HOST · DOCKER_CONTEXT · DOCKER_TLS_VERIFY · DOCKER_API_VERSION · CI · CLICOLOR_FORCE

Efficiency formula: score = Σ min-size-per-path / Σ cumulative-size-per-path; wastedBytes = Σ cumulative size of paths seen in ≥2 layers or removed