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
--sourcebinds to the same field ascontainer-engine. The image source is resolved inPostLoad(): if the image argument carries ascheme://prefix (DeriveImageSource), that wins; otherwise the source is parsed fromcontainer-engine/--source(ParseImageSource).docker-taris accepted as an alias fordocker-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-ciis 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-friendlyUIKeybindingsstruct and the runtimekey.Bindings. Adding a binding means adding a field with matchingyaml/mapstructuretags and registering a default inkey.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 underblobs/are tried as gzip/estargz, then zstd, then plain tar, then JSON. This is howdivesupports uncompressed, gzip, estargz, and zstd layer compression. manifest.jsondrives layer ordering when present; for pure OCI images (nomanifest.json) the config blob is located viaisConfig()and a synthetic manifest is built.- Each layer tar becomes a
filetree.FileTreeviaprocessLayerTar→AddPathfor every entry; missing config/manifest yieldscould 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)
FileInfoholds tar metadata plus an xxhash content hash (getHashFromReader).Compare()returnsUnmodifiedonly when type, hash, mode, UID, and GID all match — otherwiseModified.FileNodeis 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..opqmarks an opaque directory and is rejected at add time.DiffTypeisUnmodified | Modified | Added | Removed;merge()collapses differing child diffs intoModifiedso 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 itsDiffTyperelative 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
EfficiencyDataand surfaced as the inefficient files list (sorted by cumulative size). WastedBytes= Σ cumulative size of inefficient files.WastedUserPercent=WastedBytes / userSizeBytes, whereuserSizeBytesexcludes 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:
--jsonset →adapter.NewExporter(afero.NewOsFs()).ExportTo(...)writes theexport.Exportmodel and returns.- CI enabled →
adapter.NewEvaluator(rules).Evaluate(...); non-pass returns an error (non-zero exit). - 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) }:
LowestEfficiencyRulefails whenthreshold > analysis.Efficiency.HighestWastedBytesRulefails whenanalysis.WastedBytes > threshold(threshold parsed viahumanize.ParseBytes).HighestUserWastedPercentRulefails whenanalysis.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.goRun()is the entry point. It sleeps ~100ms beforegocui.NewGuito dodge a documented termbox init race whendiveis the first process in a container (issue #513 territory), then entersMainLoop.app/controller.gocoordinates view updates/renders and pane switching, emitting wrapped errors (controller failed update,controller unable to switch to next pane, etc.).layout/manager.go+layout/compoundarrange 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.gobinds 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 wherediveruns 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>sodivenever 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
diveimage 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') forwindows/darwin/linux×amd64/arm64/ppc64le, stampingmain.version,main.gitCommit,main.buildDate,main.gitDescriptionvia ldflags. - Artifacts:
tar.gz(zip on Windows),deb+rpm(nfpms), a Homebrew tap (wagoodman/homebrew-dive), and multi-arch Docker manifests on bothdocker.io/wagoodman/diveandghcr.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 withgo-snapsgolden files, exercising build, CI gate, JSON output, config loading, and image loading from docker/podman/archive sources. They useDIVE_CONFIGto 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. --sourceoverloadingcontainer-engineis a known ergonomic wrinkle (the flag name and the config key differ); preserve thePostLoadderivation 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