Browse any code base as an interactive isometric archipelago in your browser. Find bugs and vulnerabilities in a First Person Mode. https://marketplace.visualstudio.com/items?itemName=sarumaj.depphunter
  • Go 74.9%
  • JavaScript 21.4%
  • Python 1.3%
  • TypeScript 1.1%
  • CSS 0.9%
  • Other 0.4%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
renovate 31cbdef20a
All checks were successful
CI / test (ubuntu-latest, Go oldest) (push) Successful in 2m26s
CI / test (ubuntu-latest, Go stable) (push) Successful in 2m27s
CI / lint (push) Successful in 1m0s
CI / extension (push) Successful in 52s
CI / vulncheck (push) Successful in 9s
CI / dist (push) Successful in 1m36s
Merge pull request 'chore(deps): update dependency @types/node to v22.20.5' (#23) from renovate/all-patch into main
2026-10-07 17:37:01 +00:00
.forgejo/workflows Adapt to Forgejo: Renovate preset, Forgejo CI 2026-10-07 16:00:40 +00:00
.github/workflows refactor: web the map's modules in a directory per part 2026-10-01 09:39:46 +02:00
.vscode fix: lint md files 2026-10-04 14:07:01 +02:00
cmd/depphunter refactor: lang cmp.Or and typed memos 2026-09-30 10:43:53 +02:00
docs feat: seen from behind, a swimmer treads water, swims and swims flat out each its own way, leant the way they go with the ring kept flat on the water, and a walker with nothing to float on treads water, fights it and goes limp and under 2026-10-05 12:38:00 +02:00
extension feat: extension open a package's page, repository and folder from the tree 2026-10-01 14:52:50 +02:00
internal feat: a swim ring to swim the bay in, the body towed along a line from behind, strides as fast as the walker goes, the tool row kept one line per hand, and the map faded under the tracker 2026-10-04 13:28:05 +02:00
scripts fix: the spacesuit's elbow and waist rings round the arms and the waist only, not across the belly, and the Blender module the models are built with pinned 2026-10-04 14:59:38 +02:00
vendor feat: resolver notes in --explain 2026-09-28 17:48:11 +02:00
web feat: seen from behind, a swimmer treads water, swims and swims flat out each its own way, leant the way they go with the ring kept flat on the water, and a walker with nothing to float on treads water, fights it and goes limp and under 2026-10-05 12:38:00 +02:00
.gitattributes fix bugs in extension shipment 2026-09-22 00:08:10 +02:00
.gitignore feat: one body model, its hands the same hands held before the eye, closing round tools and rides 2026-10-03 16:00:35 +02:00
.markdownlint.yaml feat(m5): history overlay and refactor docs 2026-09-19 21:10:55 +02:00
.markdownlintignore refactor: move the extension manifest to root 2026-09-21 14:49:45 +02:00
.vscodeignore feat: side panel menu and settings 2026-09-22 00:40:22 +02:00
CONTRIBUTING.md fix: the spacesuit's elbow and waist rings round the arms and the waist only, not across the belly, and the Blender module the models are built with pinned 2026-10-04 14:59:38 +02:00
endcard.html refactor: consolidate tools and scipts, showcase 2026-09-25 21:05:11 +02:00
go.mod fix(deps): update module github.com/odvcencio/gotreesitter to v0.55.1 2026-09-26 20:24:49 +00:00
go.sum Merge pull request #18 from sarumaj/renovate/all-minor-patch 2026-09-26 22:36:35 +02:00
icon.png fix bugs in extension shipment 2026-09-22 00:08:10 +02:00
LICENSE feat: specify license 2026-09-19 21:12:50 +02:00
package-lock.json chore(deps): update dependency @types/node to v22.20.5 2026-10-07 16:26:40 +00:00
package.json feat: ride the playgrounds and play ball on the courts, empty-handed 2026-10-02 21:09:58 +02:00
README.md fix: review findings - a held tool aimed from the eye swimming, no jumping out of a swim, the canopy's links and toggle kept from behind, the tool row drawn in rather than spilling over the tracker, downloads cached whole, and tests for what this round added 2026-10-04 14:55:30 +02:00
renovate.json Adapt to Forgejo: Renovate preset, Forgejo CI 2026-10-07 16:00:40 +00:00
tsconfig.json feat: finalize vsx 2026-09-21 22:52:10 +02:00

depphunter

CI Open VSX Release Date Open VSX Downloads

Note: This codebase was developed with the assistance of AI tools (Claude, by Anthropic). All code is reviewed and tested before being merged.

Initial view Dependency trace
Initial view Dependency trace
Walk mode Night mode
Walk mode Night mode
Flight mode Vulnerability hunting
Flight mode Vulnerability hunting

depphunter renders a code base as an interactive isometric archipelago in a browser.

  • The mainland is the repository. Directories are terraces and files are buildings, whose height is the file's line count and whose color is its language. The map is drawn as a city: facades with windows, streets with sidewalks and crossings, ramps between levels, parks, and a surrounding sea. Selecting a node dims the rest, and dimmed buildings lose their detail so that the selection remains legible.

  • The islands are external ecosystems - Go modules, a standard library, npm

    • with one building per dependency.
  • Selecting a node shows what it depends on and what depends on it, as arcs over the map with an arrow at the far end of each. Double-clicking expands or collapses a directory or a file; a file expands into its symbols.

  • Walk mode (V) presents the same map in first person on a small planet. WASD moves, the mouse looks and Space jumps. The walker holds a tool, drawn in the hands at the end of an arm; using it on a building selects the module, draws its dependency trails and marks it with a beacon for the rest of the session.

    Ten tools occupy slots 1 to 9 and 0; E cycles through the primary tools, Q through the secondary ones, and holding R opens a wheel of all of them. The seven primary tools are what the hunt is done with: a fishing rod (the default), a butterfly net, a camera, a bubble wand, a fire extinguisher, a tracking dart and a nail gun. Each has its own animation, its own aim helper and its own valid targets - the dart acts on buildings, the net, the bubbles and the extinguisher on bugs (the extinguisher also puts out fires), the rod, the nail gun and the camera on either. What a tool throws travels according to its own flight model, and the two launchers are opposites rather than variants: the tracking dart is the longest and most deliberate shot, lobbed high and steering in the air towards the wall ahead of it, one shot per click through the scope; the nail gun reaches only across a street, fires flat and fast for as long as the button is held down, and scatters. The fire extinguisher is held down in the same way. A bubble decelerates and rises; foam spreads and drops. The net throws nothing and must be brought within reach; the camera shows its lens view live on its back, and every use of it keeps the frame, except a use on a module that has already been tagged, with no bug in front of it, which reads that module the way every other tool does there. A photograph is of the city and of nothing else: neither the camera nor the hand holding it is in it — what the walker holds is drawn over the world in a pass of its own, and that pass is left out — and neither is anything the interface has put on the map, so nothing is lit by a selection, nothing is dimmed by one and no dependency arcs cross the rooftops. A screenshot (P) is the screen rather than what the camera was pointed at, so it keeps all of them.

    What it keeps goes into the photographs (G), a contact sheet of the session's pictures captioned with whatever was in the frame. Each can be saved to a PNG file, let go of, or — from the street — put back up on the camera: the camera comes out if it is not already in hand and is brought all the way up to the walker's face, square on, until the picture on its back screen — which stands in for the live view while it is there — covers nearly the whole of it. After a few seconds it goes back down and gives the hand back to whatever was in it. A click puts it away sooner. The pictures are a session and nothing more, so anything worth keeping is saved to a file.

    The four secondary tools touch nothing on the map and carry the walker instead. The grapple gun, fired with F or C, hooks the building the walker is looking at and draws them up the facade and onto the roof, from where a shot over the edge is the way down. Its claw closes on a parapet, not on a flat wall: a hook that strikes more than two stories below a roof's edge glances off, tumbles down and is reeled back in, and pulls nobody anywhere; the jet backpack flies; the swim ring lets the bay be swum, passing under the bridges rather than over them and stepping back up onto a shore that stands half a unit above the water. The latter two run on a tank, which empties only while the tool is doing its work and fills again whenever it is not, so neither is a way of getting everywhere; a gauge beside the health bar says what is left, running dry in the air is a fall, and a tool that has run out stays stopped until its tank has filled back to a quarter, when it works again by itself. The parachute is the way down that is flown: F off a roof or out of the jet throws the pilot chute, and about a second later the canopy is open over the walker — a ram-air wing on its lines, lit like anything held, swinging the view as the walker swings under it. It comes down at under three meters a second and about four units forward for every one down, and it has its own speed and heading: A and D steer, banking and sinking faster in the turn, W and S trim it faster and steeper or slower and sinking a little more, and Space flares it. A landing under it costs how fast the walker arrives rather than how far they came, so a flare begun about two meters up lands for nothing, one begun too high stalls, and a wall flown into costs what arriving at that speed would. Thrown too low, it is still opening at the ground and the landing is judged mostly as the fall it nearly was. Its tank is the pack: spent whole when it is thrown and repacked on the ground, with the canopy draped where it came down fading as it is, and putting it away while it is open cuts it loose to drift off and leaves the walker falling. It is the eleventh tool and the digits number ten, so it has a key of its own, T, and the first slot in the row. One tool of each kind is carried at a time, one to a hand — the primary in the right, the secondary in the left — so the map can be flown over and its bugs netted without putting either down. A click uses the right hand, and F, C or the middle button the left. The HUD lays the slots out the way the walker is: what the left hand carries on the left and what the right hand hunts with on the right, each group behind a small hand of its own, because a row read at a glance in the middle of something else should not have to be parsed. The rod climbs too, from the hunting hand, which leaves the other free for the jet or the swim ring: a cast that comes down on a roof winds the walker up onto it, more slowly than the grapple and on a shorter line. A fish hook is not made for brick, though, so a cast at a wall, however high, always skips off the way a glancing grapple does. The right button holds the scope, and the mouse wheel zooms the view.

    Running and jumping are paid for out of a second gauge, the walker's wind. A sprint drains it in a few seconds and each jump takes a little more; it fills again while walking or standing, and a walker who has run it out has to get a quarter of it back before they can run or jump again, so the way across a district is a series of dashes rather than one long one. Flying costs nothing: that is the jet's tank, not the walker's chest.

    The walker has a health bar. A fall is what it would be in life, measured against the walker, who is half a unit tall: nothing up to about three meters, which a jump off a terrace wall stays under, then a share of the bar that grows with the height, and the end of the walk from about seventeen meters — five or six floors — however full the backpack. Being reeled down a line counts as falling. A bug's bite costs more the worse the finding is, and deep water with nothing to float on takes all of it in a couple of seconds, the walker going under as it does: the view sinks and bobs, the water closes over it from the bottom of the screen with bubbles rising through it, and all of it drains away again if a shore is reached in time — so taking the swim ring off out over the bay is the end of that walk, and so is walking into it without it unless a shore is reached first. The bay is not a wall around the map: it is ground half a unit below the shore, so it can be stepped down into the way a curb can, waded about in, and — because the shore is further up than a step — climbed back out of, whether by somebody wading or somebody swimming in the ring. A bridge deck is where that stops. Walking off an edge into a drop is not a thing anybody means to do, so the railings have to be gone over rather than through: off a deck, or off anything else standing well above the surface, the water has to be jumped into. Every bug in the backpack raises the bar and mends by as much. At nothing the screen goes red, walk mode ends and the map returns — nothing caught is lost — and walking in again starts at full health. Away from bites and fire, the bar slowly fills again by itself.

    Each finding a scanner reported is represented by one bug, up to 140 of the most severe, except a vulnerability proven reachable, which burns as a fire instead (see Findings). A bug is shaped by its severity as well as colored by it: a critical finding is a caterpillar that crawls and never flies, a high or medium one a beetle, and a low or informational one a mite. Bugs are placed at several heights on a building's facade and around its roof as well as in the streets, each oriented to the surface it holds on to; some hold on to nothing and instead fly a circuit around the building, rising, falling and banking at the corners. Catching one opens what was reported about it. A module the walker has tagged carries a beacon in the color of the worst finding in it, and its ring on the tracker matches, so the hunt's own trophies say which of them were worth having. A tracker in the corner of the screen sweeps the surrounding map and tightens as the walker approaches a bug, so that the last part of the approach can be made on the sweep rather than by guesswork.

    A first visit is given a short introduction explaining what the shapes stand for, that the map can be walked into, and what the bugs are, and a first walk is given one of its own covering what moves, what is in each hand, what using it does, the bugs, and fire and staying alive; the help (?) documents every key and every tool, and can show either introduction again.

    Terraces are laid out as city blocks: the space between buildings forms a connected street network with sidewalks, lane markings and crossings; ramps and stairs connect levels; unoccupied lots become parks; and a bridge crosses the water to every island.

The tool runs entirely locally. It is a single binary, requires no Node.js, and makes no network request unless --online is given (see Package indexes).

Two ways to run it

depphunter is a command-line tool that serves the map over HTTP to a browser. The VS Code extension is a wrapper around that same tool: it starts a server for the open folder and displays the map in a tab beside the code. The page served is identical in both cases.

Command-line tool VS Code extension
Installation a release archive, or go install the Marketplace or Open VSX (the binary is bundled)
Invocation depphunter [path] depphunter: Open the Map, or a folder's context menu
The map opens in the default browser a tab of the editor's own, beside the code
Configuration flags, DEPPHUNTER_* variables, .depphunter.yaml depphunter.* settings, and the same .depphunter.yaml
Additionally exports to JSON, GraphML, DOT and HTML maintains one server per folder for the editor's lifetime

Contents

Command-line tool

Install

Download an archive for the target platform from the releases (checksums in checksums.txt), or build it with Go 1.27.1 or newer:

OS Architectures Archive
Linux amd64, arm64, armv7, 386, riscv64 .tar.gz
macOS amd64 (Intel), arm64 (Apple silicon) .tar.gz
Windows amd64, arm64, 386 .zip
FreeBSD amd64, arm64 .tar.gz
go install github.com/sarumaj/depphunter-cli/cmd/depphunter@latest

Usage

depphunter            # analyze the current directory and open a browser
depphunter ~/src/app  # analyze another directory
depphunter --no-open --addr 127.0.0.1:8080
depphunter --watch    # re-analyze on change and update the open map
depphunter --findings trivy.json      # place a scanner's report on the map
depphunter --export dot -o deps.dot   # write the graph and exit
depphunter --export html -o map.html  # write a self-contained map
Flag Default
--addr 127.0.0.1:0 listen address; port 0 selects a free port
--no-open print the URL rather than opening a browser
--exclude glob of paths to omit; repeatable
--max-file-size 2097152 files above this size are listed but not read
--config <path>/.depphunter.yaml configuration file to read
--theme auto auto, light or dark
--color-by language language, size, commits, churn, age or authors
--height-scale sqrt linear, sqrt or log
--style city presentation of the map: city, circuit or galaxy
--show-std false include standard-library islands
--expand-depth 0 directory depth expanded initially; 0 picks one, -1 expands all
--ui-default seed a view setting for a repository that has saved none: key=value, repeatable (see below)
--watch false re-analyze on file change and update the open map
--no-cache neither read nor write the analysis cache
--no-history do not read git history
--history-commits 10000 read at most this many commits
--resolve-depth 0 levels of transitive dependencies to resolve from lock files; -1 for all
--private (GOPRIVATE etc.) glob naming packages the organization owns; never sent to a public index or to OSV
--trust-index (none) index URL to treat as configured on this machine, so a repository naming it is unmarked
--python (VIRTUAL_ENV, .venv) Python interpreter whose installed packages resolve imports no package index has
--online false query package indexes for what the project's own files do not record
--explain false write the resolution report once the analysis is complete
--lsp resolve symbol references using the installed language servers
--lsp-timeout 5m time budget for the language servers
--findings scanner report to place on the map; repeatable, globs permitted
--no-vulns place no scanner reports and do not query the OSV database
--no-links do not follow the links in the repository's Markdown
-v, --version print the version and exit
-h, --help list the flags with their defaults
--editor auto-detected editor command template, e.g. "code -g {file}:{line}"
--embed origin permitted to frame the map, e.g. vscode-webview:; repeatable
--export write json, graphml, dot or html and exit
-o, --output stdout output file for --export

Long flags take two hyphens (--addr, not -addr), and a flag's value may be separated from it by either a space or =.

Diagnostic output - what was analyzed, the address being served, and the resolution report - is written to stdout, so that it can be piped or redirected like any other output. The sole exception is --export without -o, where stdout carries the exported document; the log is then written to stderr instead, so that a redirected export contains nothing but the export. Errors are written to stderr in all cases.

Configuration

Settings are resolved in the following order of increasing precedence: built-in defaults; the user configuration ($XDG_CONFIG_HOME/depphunter/config.yaml, or the platform equivalent); the project configuration .depphunter.yaml; the DEPPHUNTER_* environment variables (ADDR, OPEN, EXCLUDE, MAX_FILE_SIZE, THEME, COLOR_BY, HEIGHT_SCALE, STYLE, SHOW_STD, EXPAND_DEPTH, TOOL, WATCH, CACHE, EDITOR, PYTHON, HISTORY, HISTORY_COMMITS, RESOLVE_DEPTH, ONLINE, EXPLAIN, VULNS, LINKS, LSP, LSP_TIMEOUT, and the comma-separated FINDINGS, PRIVATE and TRUST_INDEXES); and the command-line flags. For exclude, findings, private and trust_indexes, the lists from every source — the user file, the project file, the environment and the flags — are combined rather than replacing one another. --private also takes in the Go toolchain's GOPRIVATE, GONOPROXY and GONOSUMDB patterns, from the environment or the file go env -w writes.

The project configuration arrives with the repository, so only the settings on an allow-list are read from it: every setting except editor (a command depphunter executes), online, python and trust_indexes, and its findings paths must stay inside the repository. A setting added later is user-only until it is put on the list. A file passed with --config is trusted like the user configuration, and must exist.

--ui-default accepts the keys theme, color_by, height_scale, style, show_std, expand_depth, tool and path_filter.

# .depphunter.yaml
exclude: [testdata, "*.pb.go"]
history_commits: 5000
ui:
  color_by: language
  height_scale: sqrt
  show_std: false
  expand_depth: 0
  tool: rod                       # walk mode: rod, net, camera, bubbles,
                                  # extinguisher, dart, nailer, parachute,
                                  # grapple, jetpack, ring
  hide_languages: [Markdown]      # filters, as the Filters panel sets them
  hide_islands: [npm]
  path_filter: "!**/testdata/**"

The browser's Save settings button writes the current color, height, style, theme, depth, walk-mode tool, standard-library islands and filters into the ui: section of .depphunter.yaml (or the --config file), keeping the file's other keys and comments.

That section is where a repository's view lives, and it is what the map reads on the way in. --theme and the rest override it, as a flag does; --ui-default sits underneath it instead, replacing only what depphunter would otherwise have started at. So --ui-default theme=dark decides how a repository that has never been saved opens, and stops deciding the moment somebody presses Save. That is what the editor extension sends its appearance settings as, so the two never disagree about which one won.

Watch mode and cache

Parse results are cached by file content under the user cache directory (~/.cache/depphunter on Linux), so that a subsequent run parses only the files that have changed; for the CPython standard library this reduces analysis from 1.2 s to 20 ms. Under --watch, depphunter monitors the directories it analyzed, re-analyzes once changes have settled for 300 ms, and pushes the resulting map to the browser, which preserves the current expansion, selection and filters and briefly highlights the files that changed.

Exports

--export (or the Export menu in the browser) writes:

Format Contents
json the complete graph document used by the interface: nodes, symbols and edges
graphml the complete graph with all attributes, for Gephi, yEd or NetworkX
dot the dependency graph for Graphviz: files, package directories and external packages, clustered by directory. Standard-library packages and files without imports are omitted
html the interactive map as a single file, requiring neither depphunter nor a network: the interface, the graph, the source text (files up to 256 KB, 24 MB in total) and the current view — colors, height, theme, depth and filters. --export html uses the configured view

Export → PNG image, or P, writes the map as currently displayed, including labels, at the screen's resolution. It is available in an exported HTML page as well.

The HTML export contains the repository's source code and, where git history was read, the names of commit authors. It should be distributed under the same conditions as the repository itself.

Dependency graphs are shallow, which leads Graphviz to lay them out long and narrow. For large graphs, unflatten -l 3 -c 5 deps.dot | dot -Tsvg -o deps.svg produces a more even result.

HTTP API

The server that hosts the map exposes a small HTTP API, which is also what the VS Code extension's side panel reads. Every request requires the session token, supplied in the cookie that the initial address is exchanged for or, when the server runs with --embed, in an X-Depphunter-Token header (or a ?token= parameter, which the event stream uses). Every request that modifies state additionally requires the header X-Depphunter-Request: 1, which a cross-site page cannot set.

Endpoint What it is
GET /api/graph the graph document: nodes, symbols and edges
GET /api/config the view the map opens with, and the server's capabilities
GET /api/file?path= the source of a file present on the map, and of no other file; 415 for a binary file, naming its type; &as=raw its bytes
GET /api/history, /api/references, /api/findings computed in the background: 202 while in progress, 204 if there is no result
GET /api/export?format=&ui= json, graphml, dot or html; ui is the view (JSON) an html export opens with
GET /api/resolution?format= how the dependencies were resolved: json (default), md or text
GET /api/events Server-Sent Events: graph, history, references, findings, selection, backpack
----------------------------- -------------------------------------------------------------------------------------------
POST /api/selection {"id": "f:src/main.go", "origin": "…"}; the map follows
GET /api/backpack?format= the collected findings as json, csv or md
PUT /api/backpack {"items": [...], "origin": "…"}; replaces the contents
POST /api/open {"path": "…", "line": 12}; opens the file in the configured editor
GET /api/locate?id= {"folder": "…"}, the directory a package is installed in on this machine; 404 when none is found
POST /api/browse {"id": "p:npm:lodash", "to": "page"}; opens a package's page, repository or folder
POST /api/settings writes the ui: section of the configuration file

origin identifies the client that made the change and is echoed in the resulting announcement, so that a client can distinguish its own change from another's. The selection and the backpack belong to the session; the backpack's durable store is the browser's, held per repository, and the page uploads it as it loads.

/api/graph carries an ETag, so that a client already holding the document may send If-None-Match and receive 304. The tag is a fingerprint of the nodes and edges rather than of the time at which they were read, so a re-analysis that finds the same project yields the same entity — which is the question a client reconnecting to a restarted server is in fact asking. The document reaches twenty megabytes for a repository of a hundred thousand nodes, and in the majority of requests nothing about it has changed.

Every announcement carries an identifier, and the stream opens with a greeting stating its current position:

event: hello
data: {"version":3,"etag":"\"9e4f…\"","seq":11,"resumed":true}

resumed is the stream's answer to the Last-Event-ID returned by the client: nothing has been announced since the event it last received, so nothing has been missed. Without it, a reconnection is indistinguishable from a first connection and any announcement made while the connection was down is lost — and since a browser retries an EventSource on its own, this occurs without the user's knowledge.

Opening files in an editor

The Open in editor button in the side panel's corner — labeled with the editor, "VS Code ↗", and pinned beside maximize and close however far the panel is scrolled — or O, opens the selected file at the line of the selected symbol. The command is taken from --editor, DEPPHUNTER_EDITOR, the user configuration or a --config file; failing those, depphunter detects a graphical editor from $VISUAL, $EDITOR or PATH (VS Code, Cursor, Zed, Sublime Text, the JetBrains IDEs and others). If none is found, the button delegates to VS Code's vscode:// URL handler. Under the VS Code extension, the command is set to the editor the extension is running in.

VS Code extension

The extension displays the map in a tab beside the code. It starts depphunter for the open folder, waits for the address it reports, and shows that address in an editor tab of its own (why). The map behaves exactly as it does in a browser tab, live updates included. It requires VS Code 1.74 or later. Its source is under extension/.

Install the extension

Install depphunter (sarumaj.depphunter) from the Visual Studio Marketplace or Open VSX; the registry serves the build for your platform. Every release also provides one .vsix per platform, each containing the binary for that platform. Select the matching build — linux-x64, darwin-arm64, win32-x64 and so on — and install it with Extensions: Install from VSIX… in the command palette, or from a terminal:

code --install-extension depphunter_1.2.3_vscode_darwin-arm64.vsix

No further installation is required. The extension and the server it starts are produced by the same release and, for a release tagged vX.Y.Z, carry the same version, so the two cannot diverge. A _universal build is also provided for platforms not listed above; it contains no binary and falls back to depphunter on PATH.

To run a different build — one under development, or a newer release on a machine whose extension has not been updated — set depphunter.path to it. An explicit setting always takes precedence over the bundled binary.

The binary the extension runs is also put first on PATH in the editor's integrated terminals, so depphunter typed there — depphunter --export html, say — is the same version as the map. Only terminals the editor opens are changed, not the shell profile; ones already open are offered a relaunch. Nothing is added where the binary is found on PATH anyway, and depphunter.addToPath turns it off.

Use

Select the depphunter icon in the activity bar. The Maps view lists the window's folders, and any subfolder mapped from the explorer while its server runs; selecting one maps it. The entry's buttons open the map and, while a server runs, restart and stop it; the view's title bar opens the map in the browser, shows the log and opens the settings. The same action is available in the command palette as depphunter: Open the Map and in the explorer's context menu for any folder.

Command What it does
depphunter: Open the Map Maps the folder, or displays a map already produced
depphunter: Open the Map in the Browser Opens the same map outside the editor, for this instance
depphunter: Restart the Server Restarts it, which is how changed settings take effect
depphunter: Stop the Server Stops it; the next invocation analyzes afresh
depphunter: Show the Server Log The server's output, verbatim
depphunter: Show the Resolution Report Which index each package resolved from, and how the walk proceeded
depphunter: Export the Graph JSON, GraphML, DOT or a self-contained HTML map
depphunter: Export the Backpack The collected findings as Markdown, CSV or JSON
depphunter: Add to the Backpack Puts a finding in the backpack without walking to its bug
depphunter: Refresh the Side Panel Reads the graph and the backpack from the server again
depphunter: Open Settings The extension's settings, documented below

One server is maintained per folder and kept until the window closes or the server is stopped explicitly: analyzing a large repository takes time, and under --watch it need happen only once. A status bar item is shown while a server is running; selecting it opens the map. If a server exits unexpectedly, a warning offers to show its log or restart it.

The panel beside the code

The same panel holds three further views below Maps, all showing the map opened most recently:

  • Dependencies presents the graph as a tree. A directory expands into its contents, a file into its imports, an island into its packages, and a package into its own dependencies, as far as --resolve-depth reached. Expanding a row requires no further request: every edge is already present in the graph the panel fetched once. A branch leading back to a node already expanded above it is shown once more, marked ↻, and left collapsed, since dependency graphs contain cycles. A package that is not pinned, or that resolves from an index this machine does not configure, is marked in the list itself rather than only in its tooltip.

  • Backpack holds the findings collected while walking the map, ordered by severity, with those absent from the most recent scan marked as resolved at the end. Removing an entry here removes it from the map's backpack as well.

  • Findings lists everything the scanners reported, most severe first and then by package or file, whether caught or not. The + beside an entry puts it in the backpack exactly as catching its bug on the map would — the same entry, recorded against the same building — and the map's bug stops walking; an entry already in the backpack is marked so and offers × to take it out again. depphunter: Add to the Backpack in the command palette asks which of those not yet caught to add. A catch on the map is marked here as it happens.

The Dependencies view's title bar holds the resolution report, the graph export and a refresh, and a file's row has a button that opens the file; the Backpack's title bar exports it.

A package's row has buttons that open its page on the index it is published on and the repository of its source in the browser, and one that reveals the folder it is installed in: in the Explorer when that is in the workspace — a node_modules or a vendor directory — and otherwise, for a shared cache such as Go's module cache or ~/.m2, in a window of its own or the system's file manager. The map's details panel offers the same three links, which in the editor's tab the server hands to the extension to open.

The two views and the map form a single interface: selecting a row selects the corresponding building on the map, and selecting a building on the map expands the tree to its row. The server is what makes this so — it holds the selection and the collected findings while the map is open and announces changes to either (API) — which is also why a second browser tab stays in step.

Settings

The settings, in the groups the Settings editor presents them in. Each names the flag it passes, and passes it only when set to something other than depphunter's own behavior — an enum left at default, a number left empty, a switch left at depphunter's default — so that a folder's .depphunter.yaml continues to govern everything not set here.

Setting Default Flag What it does
General
depphunter.path (empty) The binary to run. Empty selects the bundled binary, falling back to depphunter on PATH.
depphunter.addToPath true Put that binary first on PATH in the editor's terminals.
depphunter.openIn webview webview (a dedicated tab), simpleBrowser (the built-in browser) or externalBrowser (the system default).
depphunter.watch true --watch Re-analyze on file change and update the map.
depphunter.config "" --config A configuration file to read instead of the folder's .depphunter.yaml, relative to the folder.
depphunter.editorCommand "" --editor The command Open in editor invokes. Empty selects the current editor.
depphunter.args [] Further arguments, one per entry, appended last. Usage lists them.
Analysis
depphunter.exclude [] --exclude Globs of paths to omit.
depphunter.maxFileSize (empty) --max-file-size Files larger than this many bytes are not read.
depphunter.resolveDepth (empty) --resolve-depth Levels of transitive dependencies to resolve from lock files; -1 for all.
depphunter.online false --online Query package indexes and the OSV database over the network.
depphunter.cache true --no-cache Read and write the analysis cache.
depphunter.history true --no-history Read git history for the history overlays.
depphunter.historyCommits (empty) --history-commits Read at most this many commits.
depphunter.private [] --private Globs naming the packages the organization owns. Never sent to a public index or to OSV.
depphunter.trustIndexes [] --trust-index Index URLs to treat as configured on this machine, so a repository that names one is not marked.
depphunter.explain false --explain Write the resolution report to the output channel whenever the map is built.
Appearance (seeds: a repository that has saved a view of its own keeps it)
depphunter.style default --ui-default city, circuit or galaxy.
depphunter.theme default --ui-default auto, light or dark.
depphunter.colorBy default --ui-default language, size, commits, churn, age or authors.
depphunter.heightScale default --ui-default linear, sqrt or log.
depphunter.expandDepth (empty) --ui-default Directory levels expanded initially; 0 picks for you, -1 expands all.
depphunter.showStd false --ui-default Include standard-library islands.
Findings
depphunter.findings [] --findings Scanner reports to place on the map, relative to the folder. Globs permitted.
depphunter.vulns true --no-vulns Place reports on the map and, under online, query the OSV database.
depphunter.links true --no-links Follow the folder's Markdown links and report those that lead nowhere.
References
depphunter.lsp false --lsp Resolve symbol references using the installed language servers.
depphunter.lspTimeout "" --lsp-timeout Time budget for the language servers, e.g. 90s.

What is left out is left out on purpose: --addr, --no-open and --embed are how the extension hosts the map and are not for changing, and --export (with --output) writes a file and exits rather than serving. --python has no setting of its own; pass it through depphunter.args.

They are ordinary settings, so they can be set per workspace in .vscode/settings.json:

{
  "depphunter.style": "circuit",
  "depphunter.findings": ["reports/trivy.json", "reports/*.sarif"],
  "depphunter.exclude": ["vendor"],
  "depphunter.lsp": true,
  "depphunter.resolveDepth": 1
}

A server reads its settings at start-up, so a change takes effect on the next depphunter: Restart the Server; the extension offers to perform the restart. Anything the settings do not cover belongs in depphunter.args or in the project's .depphunter.yaml, which the server reads as usual.

Open in editor opens the file at the line currently in view, in this editor: the extension locates this editor's own command-line launcher (on Windows, its executable) and passes it to the server, rather than leaving the server to find whatever is on the PATH the extension host inherited. depphunter.editorCommand overrides this where the file should be opened elsewhere.

Remote workspaces

Over SSH, WSL and dev containers the port is forwarded to localhost on the local machine and the extension works unchanged. In Codespaces the forwarded address is a public hostname, which the server rejects as a DNS-rebinding attempt, since it answers only to localhost, 127.0.0.1 and ::1; there, run depphunter from a terminal instead. On vscode.dev without a remote the extension does not load at all, since it needs a Node extension host.

Why the map is in a tab of its own

The editor's built-in browser is the natural place for a local page, but it places that page in a sandbox of its own, and the pointer lock is among the capabilities that sandbox withholds. Walk mode therefore cannot capture the mouse there, and since a sandbox can only be narrowed further down the frame chain, the page has no means of recovering it.

The map is opened in a dedicated editor tab instead: one webview containing one iframe, and that iframe carries no sandbox attribute. Adding none removes nothing, so the pointer lock the editor granted the webview is preserved. What is given up is an address bar and a back button, on a single page that requires neither. depphunter.openIn may still select simpleBrowser, where walk mode reports the limitation and turns the view by dragging instead.

How the framing works

In either case the page is inside a webview and is therefore framed by origins belonging to the editor. depphunter refuses to be framed by default, so the extension starts it with --embed, naming every frame above the page — all three, because frame-ancestors is evaluated against the entire chain and not only the frame immediately containing the page:

--embed vscode-webview: --embed vscode-file: --embed https://*.vscode-cdn.net
  • vscode-webview: is the webview, which is given an origin of its own for every session (vscode-webview://<uuid>), so there is no exact name to give.
  • vscode-file: is the editor's window, served from vscode-file://vscode-app in the desktop editor.
  • https://*.vscode-cdn.net is the webview in the browser build.

If any one of them is omitted, the browser refuses the page before it loads, leaving an empty tab with the reason recorded only in the webview's own developer tools. No other origin may frame the page; any that attempts to is refused in the same way. The remaining effects of this mode are described under Inside an editor.

In this mode the session token remains in the address rather than being exchanged for a cookie, because a cookie set by the map would be a third-party cookie within the frame and would never be returned. This is why the extension reads the address from the server's own output rather than constructing it from the port: that address is the only place the token appears. The extension always starts the server in this mode, so even a map it opens in an external browser keeps the token in the address.

The map

The remainder of this document describes the map itself, which behaves identically whether it was started by the command-line tool or by the extension. Where a section names a flag, the extension passes it through its own setting if one exists (see Settings; depphunter.style and the other appearance settings only seed the default) and through depphunter.args otherwise.

Keyboard & mouse

Panning is bounded at the point where the center of the view lies a quarter of the map's extent (plus a small margin) beyond its edge, and zooming out at the point where the map occupies roughly a third of the view. In walk mode the walker may travel 3 units out over the water and 12 units above the tallest building.

Drag / right-drag / wheel pan, orbit, zoom
Click / double-click select, expand or collapse; double-click on open ground enters walk mode
Middle-drag zoom
Enter, Backspace expand or collapse the selection, select parent
→ ← in the panel expand or collapse a dependency row
Enter while reading close the details and resume
E Q in walk mode the next tool for the right hand, the left hand
Q E rotate by 90°
Home fit the map to the view
R reset the view
+ - expand or collapse one level throughout
/ search files, symbols and packages
O open the selected file in the editor
P write the map to a PNG image
Legend click show or hide a language
Pin click read the findings recorded on a building
Find in the details search a file's source: every match highlighted, Enter/Shift+Enter or ↓/↑ step through them, Esc clears
+ beside a finding add it to the backpack
L list every finding the map shows, to read or add to the backpack from (arrows move, Enter opens, + adds)
B open the backpack
G open the photographs the camera has taken; from the street each can be put up on the camera and looked at there
X open the export menu
K save settings to the config file
The figure the walker's last position in walk mode
Esc close the photographs, the backpack or the findings list, or clear the selection
V enter walk mode ^
? show all the controls

In walk mode:

Mouse look. The pointer is captured at the reticle; Esc releases it and a click on the map captures it again. Where it cannot be captured at all — a frame that withholds the pointer lock — walk mode reports this once, and a click then uses the tool rather than requesting the lock again
R the tool wheel: every tool at once, the primary ones down its right side and the secondary ones down its left, with an empty left hand at the bottom. Hold R, point with the mouse and release; or tap R to leave it up and take what is under the cursor with R, Enter or a click. Esc, the right button, or releasing with the cursor still in the middle changes nothing. While it is up the walker is held where they stand and the city behind it is blurred: nothing moves them, spends a tank or bites them, so changing hands costs no time. Pointing at a tool already in hand keeps it
T 1 2 3 select a secondary tool, or press the same key again to put it down. Only one is carried at a time. The parachute has T, because the ten digits are taken
4 … 0 select a primary tool
E the next primary tool, cycling. The right hand is never empty
Q the next secondary tool, and after the last of them an empty left hand — which is how the walker comes down out of the air or steps off the water deliberately. Five presses return to the starting state
W A S D/arrows move and turn; Shift runs, which spends the walker's wind
Space jump, which costs a little wind; while flying, ascend; under a parachute, flare
Jet backpack in hand flight. While flying, W and S move along the view direction — looking down and pressing W descends — and C descends vertically. F (or middle click) opens the throttle for a burst. The view banks into turns and sideways moves, and levels out again on landing
Swim ring worn the bay can be swum, slower than walking, down in the water to the chest, passing under the bridges rather than over them; taking it off over deep water drowns the walker
Parachute in hand F (or C, or middle click) throws it open off a roof or out of the jet; it takes about a second to open. Under the canopy A D/arrows steer, W S trim faster or slower, and Space flares; the landing costs the touchdown speed, and a flare about two meters up costs nothing. Putting it away while open cuts it loose
Click use the right hand; held down, the nail gun and the extinguisher keep firing. A module within reach is selected, and a bug that is caught is displayed and retained
F, C / middle click use the left hand. A secondary tool selects and catches nothing: the grapple hooks the building being looked at, the jet gives a burst of thrust. While flying, C descends instead
B the backpack, from the street as well as from the map
X / K open the export menu; save the current view to the config file
H stow or draw both hands. A stowed tool remains functional and throws from the walker's eye
Hold right button look through the scope
Enter show the details of whatever the reticle is on, as a second use of the tool would. The street softens around the building, which stays sharp with a little of what is around it. This releases the pointer; a click on the map resumes
Wheel zoom
+ - (or [ ]) planet radius, and therefore curvature
V / M / Esc return to the map (Esc first releases a captured pointer). Re-entering walk mode restores the previous position

The map draws the walker at their last position, as a figure facing the direction they faced — under a canopy, if they left walk mode under one — and re-entering walk mode restores that position, still under it, unless a node was selected on the map in the interim, in which case the walker is placed at that node instead, with the parachute packed.

On foot the bay can be stepped down into and waded in, but deep water drowns a walker without the swim ring, and every island is reachable by bridge; the walker may travel at most 3 units out over the water. The ground beneath the walker is never a target, so aiming at the street selects nothing. While a bug is being caught, the street softens around it for the second the catch takes, so it is the one thing in focus. Expanding and collapsing are reserved to the map view, since either rebuilds the entire city and is disorienting from street level. The list of controls collapses once the walker begins to move; ? displays all of them.

Styles

The same map, presented in three ways (--style, or the Style menu):

Style What it is
city Buildings of seven types - residential slabs with balconies, glass office towers, brick walk-ups, concrete panel blocks, art-deco towers with setbacks, warehouses, and shops under flats - picked per file from its name and its proportions, never by color; streets with crossings and parks, wooded shores, and bridges between the islands
circuit A printed circuit board: chip packages with rows of pins, heatsinks in place of tall buildings, copper traces along every street with vias set into them, solder pads and silkscreen around each part, capacitors in place of trees and lit LEDs in place of lamps. Beyond the edge of the board lies the backplane it is plugged into, which is live: charge travels along its tracks, indicating that it cannot be walked on
galaxy Platforms suspended in darkness: crystal spires with strata of light and star-like windows, joined by luminous conduits. In place of sea and sky there is the band of the galaxy with its dust lanes, two nebulae behind it and three layers of stars in front. The void the platforms hang in drifts in layers at three different speeds, so the map view redraws continuously under this style (and under circuit)

Only the environment differs. The colors that carry data — the language palette, the history overlays, hover and selection — are identical in all three (packages take a tint matching the style), so a style alters the presentation and never the reading of the code. Nothing else differs either: the same layout, the same streets, the same walk.

A city building's type comes from its file's name and its proportions, so it is the same building on every run. Near the walker, or once the map is zoomed in close, the buildings grow balconies, awnings, cornices, water tanks, air-conditioning units, antennas and roof railings. They are decoration and nothing more: the walker, the grapple and the jetpack meet the building's own walls, and a balcony stands out of a facade by less than the walker's width. The tallest art-deco towers step back in tiers whose top is the building's height, and their ledges are roofs like any other.

Findings

depphunter runs no scanner; it reads the reports an existing pipeline has already produced. Point --findings at the JSON emitted by CI — the flag is repeatable and accepts globs — and each report is placed on the map:

Tool written by
govulncheck -format json the advisory, and the call site that reaches it
npm audit --json npm 7 and later, and the npm 6 advisory table
trivy … --format json vulnerabilities, misconfigurations and secret hits
osv-scanner --format json lock-file scans
golangci-lint run --out-format json one finding per issue
eslint -f json one finding per message

The format is determined from the report's structure rather than from its file name, so the names a pipeline assigns are immaterial:

govulncheck -format json ./... > reports/govulncheck.json
trivy fs --format json -o reports/trivy.json .
depphunter --findings 'reports/*.json'

Each finding is placed on the node it concerns: a vulnerability on the package it affects, a linter's diagnostic on the file it refers to. A directory carries the most severe finding beneath it. Severities are normalized onto one scale — critical, high, medium, low, info — derived, for Trivy and OSV entries, from the advisory's CVSS v3 vector where one is present (npm audit's own rating is used as given), because the severity a distribution assigns frequently disagrees with it; Trivy classifies CVE-2020-8203 as MEDIUM against a vector scoring 9.8. A linter's "error" is deliberately not treated as a critical advisory: golangci-lint and eslint findings are capped at medium, since otherwise the streets would fill with bugs representing missing comments; Trivy's misconfigurations and secret hits keep Trivy's rating. An advisory govulncheck finds no call into is capped at low.

Under --online, depphunter additionally queries OSV for every external package the map pins to a version — batched queries of 500 packages each, followed by the advisories it matched — covering Go, npm, PyPI, crates.io, Maven, NuGet (C# and F# packages alike), GitHub Actions, Conan (as OSV's ConanCenter; vcpkg has no OSV ecosystem), Composer (as Packagist), RubyGems, Swift packages (as SwiftURL, by URL), pub, Hex (Elixir, Erlang and Gleam packages alike), CRAN, Bioconductor, Hackage, opam and Julia; OSV has no ecosystem for Terraform modules and providers, Buf Schema Registry modules, CocoaPods, Carthage, LuaRocks, Wally, CPAN, Zig, Bazel modules and repositories (the Maven, PyPI, Go, npm and crates.io packages Bazel's module extensions install are asked about as such), Nix flake inputs, nixpkgs packages, Elm packages, PureScript packages, Crystal shards, the GitHub, git and HTTP dependencies Paket fetches, dub packages, fpm packages, haxelib libraries, Alire crates, Racket packages, Quicklisp projects, Soldeer packages, git submodules (a Hardhat project's npm packages are asked about as npm), nimble packages, jsonnet-bundler packages, CUE modules (the Go modules CUE definitions were generated from are asked about as Go), Dhall packages or Puppet modules by name and version.

A package fixed to a full git commit (40 hex digits, or 64 for SHA-256) is also asked about by that commit — OSV matches a commit against the repositories and commit ranges its advisories record, whatever the ecosystem — in the same batch, so the git dependencies of the ecosystems above get an answer after all. The commit is read from:

Pin Ecosystems
a version that is a full commit Carthage, Zig, Paket GitHub and git files, CMake FetchContent, Terraform git module sources, SwiftPM revisions, git submodules, jsonnet-bundler, dub, fpm, nimble, haxelib, Alire, Quicklisp, Soldeer, CocoaPods, Bazel git_override/git_repository, Puppet git modules, PureScript git packages, and the git dependencies of mix, rebar3, Gleam, CRAN remotes, Hackage, opam, Clojure and Julia
<version>+git.commit.<sha> Crystal shards
github:owner/repo#<sha>, git+<url>#<sha> npm
the commit a lock file records for a git dependency package-lock.json, yarn.lock, pnpm-lock.yaml and bun.lock
dev-<branch>#<sha>, and a composer.lock branch's source reference Composer
the lock's GIT section revision Bundler
the Cargo.lock source git+<url>#<sha> Cargo
a direct reference name @ git+<url>@<sha> (which now pins) PyPI
the locked rev (the map shows it shortened) Nix flake inputs, niv and npins

Only a commit of a repository on a public forge (github.com, gitlab.com, bitbucket.org, codeberg.org, sr.ht) is sent — or, for shards, Alire, haxelib, Soldeer, fpm, nimble and Quicklisp packages that name no repository, one their plugin found on a public forge (a repository elsewhere is recorded as the package's origin and makes it private). Nothing a --private pattern or GOPRIVATE matches, by package name or by repository, is sent. A package that is private only because it was installed from a public repository instead of an index (a mix git dependency, a Bundler GIT gem, an npm git dependency) is asked about by its commit alone, never by its name and version, and a version that is itself a git reference is not sent as a version. A shortened commit (Bun's seven digits) is not a pin OSV can be asked about. What a commit matched is reported by OSV (git commit), with the fixed commit of the advisory's range for that repository; an advisory the package's name and version already returned, under its id or an alias, is shown once.

Floating packages are not queried, since they resolve to a different version on the next installation. Answers are cached for six hours. --no-vulns disables all of this.

Viewed from above, every building carrying findings bears a pin, colored by the most severe of them and growing taller with their number, so that the red pins identify where to look next from across the map. Hovering over a pin gives the count; selecting it displays the findings. The side panel lists them under Findings, ordered by severity, each expanding in place to show the description, the fixed version and the advisory link. A collapsed directory lists the findings beneath it as well, so that a district marked red for something several levels down can be reached from its pin.

In walk mode the findings appear in the streets: each is a bug patrolling the building it belongs to, colored by severity and shaped by it — a caterpillar for a critical finding, a beetle for the middle of the range, a mite for a note. Catching one with the current primary tool — the net, the bubble wand and the fire extinguisher are meant for it — displays what it carries. The HUD reports how many remain, and a bug left uncaught bites: the worse the finding, the more it costs.

A vulnerability govulncheck proved reachable is not a bug but a fire: the package and the file that calls into it burn, on the map as well as in the street, and the fire spreads to the files that import them until it is put out with the extinguisher.

The backpack (B) is shared between the two views. Adding a finding with the + beside it in the panel is equivalent to catching its bug in the street; in both cases the bug stops moving. Its contents survive a re-layout, a depth change and a reload. An entry is kept until it is cleared; once the scanners stop reporting it, it is struck through rather than removed, so that a resolved finding remains visible as such.

The findings list (L, or Findings in the toolbar) is the map view's way to every finding at once: all of those on what the filters leave on the map, most severe first and then by package or file, each with its identifier, title and location. Pointing at a row highlights its building and picking it selects the building and opens the finding in the panel. The + at the end of a row is the same catch as the panel's + and the street's: the same backpack entry, the same building, the same update to the server — so the bug stops walking without anybody walking to it. A finding already in the backpack reads "in the backpack", and its ✓ takes it out again. The list is worked through with the keys as well: the arrows move, Enter opens, + adds and Esc closes. In walk mode the findings are bugs, and the list is left to the map.

A repository is larger than it appears from within it, so the corner of the walk HUD carries a tracker: a sweep centered on the walker and rotating with them, with one dot per bug in its severity's color, one ring per module already tagged with the tool, and an arrow at the rim for each bug or fire beyond its range. The range adapts to what remains, and beneath it are the distance to the nearest bug and what that bug carries.

Git history

In a git work tree, depphunter reads the history of the analyzed files — by default the most recent 10,000 non-merge commits; --history-commits changes the limit and --no-history disables it — in the background once the map is displayed, and caches the result per commit. The color menu then offers:

Mode color shows
Commits commits per file; the per-file mean for collapsed directories
Lines changed lines added plus lines deleted; the per-file mean for collapsed directories
Last change recency of the last change, with recent changes strongest
Authors the number of distinct authors

Files with no commits in range are given a separate neutral color. The Since slider in the legend restricts commits, lines changed and authors to a time range; tooltips and the side panel report the same figures, and the panel additionally lists the principal authors. Renamed files retain the history of their former names. Under --watch, a new commit updates the overlay.

git is only ever asked what is on disk: it is never allowed to fetch, never consults a credential helper and never prompts. A partial clone (--filter=blob:none) lacks the file contents that counting lines and finding renames read, so its history is read from the commits alone: commits, last change and authors are shown, "Lines changed" is not offered, and renames are not followed.

Versions and pinning

Every external package carries the version the project resolves it to — or, where nothing resolves it, the range it is declared with — and whether anything fixes it at that version. Lock files, exact specifiers (==1.2.3, RequiredVersion), single-version ranges ([1.2.3]), commits and digests pin a dependency; ranges, wildcards, snapshots and mutable tags do not. A dependency that nothing pins is drawn in amber (violet under galaxy), labeled ⚠ floating in the side panel, and marked in its tooltip. Where a lock file resolved a range, the panel reports both: 4.3.1, requested as ^4.2.0.

Ecosystem pinned by floats on
Go modules the version in go.mod, which the build selects; a script's go install x@v1.2.3 — (a require always names a version); a script's @latest or @v1.2
npm package-lock.json (or npm-shrinkwrap.json), pnpm-lock.yaml, yarn.lock, bun.lock (in each, a git dependency by the commit it names), an exact 1.2.3 any range, including 1.2, which denotes 1.2.x; a tarball URL; a git dependency locked to a branch or tag only; only bun.lockb
crates.io Cargo.lock, a script's cargo install x@1.2.3 the manifest alone, where "1.2.3" denotes ^1.2.3
PyPI index-url, PIP_INDEX_URL, -i; a uv index with default = true, UV_DEFAULT_INDEX; Poetry's default and primary sources, asked in order; a Pipfile's first source; a PDM source or [pypi] url named pypi, PDM_PYPI_URL extra-index-url, PIP_EXTRA_INDEX_URL; any other uv index, UV_INDEX; a supplemental Poetry source; a Pipfile's other sources; any other PDM source
Maven a plain version, [1.2.3], gradle.lockfile (and Gradle 6's gradle/dependency-locks/*.lockfile) ranges, LATEST, RELEASE, -SNAPSHOT, dynamic 1.+ and latest.*, unexpanded ${…}
NuGet an exact version, [1.2.3], Paket's = 1.2.3, paket.lock, packages.lock.json wildcards (2.*) and ranges, Paket's ~> and >=, no version in Paket or #r
Paket remote files paket.lock's commit, a commit in paket.dependencies a branch, tag or tag range, no ref, every HTTP file
PowerShell Gallery RequiredVersion (a manifest, #Requires, Install-Module), a bare Install-PSResource -Version or PSDepend version ModuleVersion and -MinimumVersion, which are minimums; a NuGet range; latest
GitHub Actions a full commit SHA tags, branches, or no ref
GitLab CI includes a commit tags, branches, templates, remote includes, or no ref
Container images an @sha256: digest tags
vcpkg an overrides entry version>=, which is a minimum; no version and no baseline
Conan an exact reference (zlib/1.2.13), conan.lock a version range ([>=1.0 <2], [~1.2])
Composer composer.lock, installed.json, a bare 1.2.3 or 1.2, dev-main#<sha> ^, ~, *, 1.2.*, alternatives, ranges and branches
RubyGems Gemfile.lock (a git gem by its revision), a bare 1.2.3, = 1.2.3 ~>, >=, <, !=, several requirements, no version
Swift packages Package.resolved, exact:, a bare "1.2.3", revision: from:, .upToNextMajor, .upToNextMinor, ranges, branch:
pub pubspec.lock (a git package by its commit), a bare 1.2.3, a git commit ^, ranges, any, no constraint, a git branch or tag
Hex mix.lock, rebar.lock, Gleam's manifest.toml, a bare 1.2.3, == 1.2.3, a git ref commit ~>, >=, or and and requirements, a git branch or tag
CRAN, Bioconductor renv.lock, packrat.lock (a GitHub package by its commit), (== 1.2.3) (>= 1.2), no version, a Remotes branch or tag
Hackage cabal's plan.json, cabal.project.freeze, stack.yaml.lock, extra-deps, ==1.2.3, a repository commit ^>= and other ranges, no version, a repository tag or branch
Terraform modules a registry version of 1.2.3 or = 1.2.3, the version terraform init installed (.terraform/modules/modules.json), a git ref commit ~> and other ranges, no version, a git tag or branch, no ref, an archive
Terraform providers .terraform.lock.hcl (the root module's, for the modules it calls), a single exact constraint ~>, >= and other constraints, no constraint
jsonnet-bundler jsonnetfile.lock.json (a nested project without a lock: the lock that installs it), a commit as version a branch (main, master) or no version; a tag (v1.2.3) is shown, neither
CUE modules the exact v of a cue.mod/module.cue dependency (the modules system selects versions as Go does) a dependency without v
Dhall packages a sha256: integrity check on the import (Dhall refuses other content) a URL naming no version, or a branch; a version or commit in the URL is shown, neither
Puppet modules a Puppetfile's exact version, a git :commit (or a hash as :ref), a fixture's exact ref :latest, no version, a :branch, a metadata.json range; a :tag is shown, neither
Buf Schema Registry buf.lock, a commit ref (:0123…), a plugin's exact version a label, tag or branch ref, no ref, a plugin without a version
CMake FetchContent a GIT_TAG commit, a URL_HASH, an archive of a commit a branch GIT_TAG (main, origin/…), no GIT_TAG, a download without a hash
CocoaPods Podfile.lock (a git pod by its checkout commit), a bare '1.2.3', '= 1.2.3', a :commit ~>, >= and other ranges, no version, a :branch, a git pod without a reference
Carthage Cartfile.resolved, == 1.2.3, a quoted commit ~>, >=, no requirement
LuaRocks luarocks.lock, == 1.2.3 or a bare 1.2.3 in a rockspec (LuaRocks reads it as ==) ~>, >= and other constraints, no version
Wally wally.lock, =1.2.3 a bare 1.2.3 (a caret range in Wally), ^1, other ranges
CPAN cpanfile.snapshot (Carton), == 1.2 in a cpanfile or META prerequisites a bare 1.2 (a minimum in CPAN::Meta), >= 1, < 2 and other ranges, 0 or no version
opam *.opam.locked, dune.lock/, {= "1.2"} or (= 1.2), a pin-depends commit >= 5.6 & < 6 and other ranges, no constraint, a pin-depends branch or tag
Julia Manifest.toml (and Manifest-v1.11.toml), =1.2.3 in [compat], a [sources] commit rev a bare 1.2 (a caret range in Pkg), ~1.2, >= 1, 1.2 - 1.5, no [compat] entry
Zig a .hash in build.zig.zon (Zig verifies the download), a commit in the URL a branch archive (refs/heads/), a URL without a ref or hash; a tag is shown, neither
Clojure (Maven) an exact :mvn/version or Leiningen version (Maven's rule), a full :git/sha RELEASE, LATEST, ranges, snapshots; a :git/tag alone is shown, neither
Bazel modules MODULE.bazel.lock, a bazel_dep version, single_version_override, an override's commit or integrity no version; a git_override branch; a git_override tag is shown, neither
Bazel repositories an http_archive sha256 or integrity, a git_repository commit, an archive of a commit a branch (archive or branch =), no ref and no hash; a tag is shown, neither
Nix flake.lock, a commit (rev=, /<commit>) or narHash in the reference, niv and npins pins a branch (nixos-24.05, refs/heads/), a channel, <nixpkgs>, a registry name, no ref; a tag is shown, neither
Elm an application's elm.json (exact versions of direct, indirect and test dependencies) a package's elm.json ranges (1.0.0 <= v < 2.0.0)
PureScript spago.lock, a registry version in extraPackages, a git commit (ref, a packages.dhall version) >=7.0.0 <8.0.0 and bower's ^6.0.0, no package set; a package set's name or a git tag is shown, neither
Crystal shards shard.lock, a commit:, a path dependency (in the repository) ~>, >= and other ranges, a branch:, no requirement; a tag: or an exact version: is shown, neither
dub dub.selections.json, ==1.2.3 or a bare 1.2.3 (exact in dub), a repository at a commit, a path ~>, ^, >= and other ranges, *, a ~branch, no version
fpm a git rev, a registry dependency's v, a path a git branch or no ref, a registry dependency without v, a metapackage's "*"; a git tag is shown, neither
haxelib a lix pin (haxe_libraries/<name>.hxml: a haxelib version or a git commit), -lib x:1.2.3, an exact haxelib.json or <haxelib version>, a git: commit no version, a git branch or a bare git URL; a git tag or a lix tag is shown, neither
Alire crates alire.lock, =1.2.3 or a bare 1.2.3 (exact in Alire), a pin to a git commit or a version, a path pin (in the repository) ^, ~, >=, /=, * and & combinations, a pin to a git branch or without a commit, a path pin elsewhere
Racket packages a git source's #<commit>, a #:checksum (raco keeps no lock file) a #:version (a minimum), no version, a git branch; a version tag is shown, neither
Quicklisp qlfile.lock, a dist version (ql x 2023-10-21, ql :all, the lock's dist for every project), a git :ref commit, an ocicl.csv digest :latest, a git :branch or no ref, (:version x "1.2") (a minimum), a project without a qlfile; a git :tag is shown, neither
Soldeer soldeer.lock, an exact "5.0.2" in foundry.toml, a git rev commit a version requirement (^1.0.0), a git branch or no ref; a git tag is shown, neither
Nimble packages nimble.lock, atlas.lock, == 1.2.3 or a bare 1.2.3 (exact in nimble), a #<commit> a range (>=, ^=, ~=, &), no version, #head or a branch; a #tag is shown, neither
Git submodules the commit git records for the submodule (its gitlink) no recorded commit (a .gitmodules branch is shown as the version)

A package a shell script installs (pip install, npm install -g, go install, cargo install, gem install) follows its ecosystem's row; one installed without a version, or at latest, floats.

The JSON and GraphML exports carry requested and floating per package.

Dependencies of dependencies

--resolve-depth extends the graph beyond what the code imports directly to what those packages themselves require: 1 adds one level, 2 adds two, and -1 continues as far as the available information reaches. That information comes from the lock files the repository already carries and from what package managers installed into it (or, for Elm, Zig, dub and CUE, into this machine's package cache); nothing is fetched, and the analysis remains offline.

Lock file gives
package-lock.json, npm-shrinkwrap.json each copy's requirements, found up its path
pnpm-lock.yaml (v5-v9) packages: or, in v9, snapshots:
yarn.lock (classic and Berry) each entry's dependencies, by descriptor
bun.lock (Bun 1.2) each package's dependencies, nested ones first
Cargo.lock dependencies per crate
uv.lock, poetry.lock, pdm.lock each distribution's own requirements
an installed Python environment each distribution's Requires-Dist
conan.lock (Conan 1, graph_lock) the requires of each node
composer.lock, installed.json each package's require, without the platform
Gemfile.lock the dependencies listed under each spec
.build/checkouts/*/Package.swift the checked-out package's .package lines
mix.lock each Hex package's requirements
manifest.toml (Gleam) each package's requirements
renv.lock, packrat/packrat.lock each R package's requirements
dist-newstyle/cache/plan.json what each package of cabal's build plan depends on
Podfile.lock the pods each pod's specs depend on
Carthage/Checkouts/<name>/Cartfile a checked-out dependency's own entries
wally.lock each Wally package's dependencies
rockspecs in lua_modules/, .luarocks/ an installed rock's dependencies
cpanfile.snapshot (Carton) each distribution's requirements
dune.lock/ (dune package management) each package's depends
Manifest.toml (Julia, formats 1 and 2) each package's deps
zig-pkg/<hash>/ or Zig's global cache a fetched package's own build.zig.zon
MODULE.bazel.lock (before Bazel 7.2) the resolved module graph (moduleDepGraph)
maven_install.json (rules_jvm_external) each artifact's dependencies
flake.lock (versions 5 to 7) each input's own inputs, follows resolved
elm.json of packages in ELM_HOME an installed Elm package's dependencies
spago.lock (PureScript) each package's dependencies
manifests of packages in .spago/ an installed package's dependencies
lib/<shard>/shard.yml (shards) an installed shard's dependencies
lib/.shards.info (shards) the versions of the shards installed in lib/
recipes of packages dub fetched a fetched dub package's dependencies
build/dependencies/<name>/fpm.toml a fetched fpm package's dependencies
lix's haxe_libraries/<name>.hxml a pinned library's -lib lines
haxelib.json of installed haxelibs an installed library's dependencies
alire.lock (Alire) each release's depends-on, at the chosen version
alire.toml of crates Alire fetched a fetched crate's depends-on
nimble.lock (nimble) each package's dependencies
.nimble of installed nimble packages an installed package's requires
jsonnetfile.json in jb's vendor/ an installed package's dependencies
module.cue in cue's module cache a fetched module's deps
metadata.json in r10k's modules/ an installed Puppet module's dependencies
.asd in .qlot/, ocicl's systems/ an installed system's :depends-on
dependencies/<name>-<version>/ (Soldeer) an installed package's [dependencies]
.terraform/modules/modules.json an installed module's own module calls

Packages added in this way are marked transitive, meaning that no file in the repository imports them. Edges between packages are of kind depends, as distinct from the import edges that originate at a file, so that the count of files importing a package remains exactly that. Two versions of one package remain a single building, so an edge between packages is an edge between names. For npm, a package reached this way takes the version of the copy its dependent loads: node_modules/a/node_modules/b before the hoisted node_modules/b, the yarn.lock entry whose descriptors hold the dependent's range, the version pnpm-lock.yaml names. A dependency under an alias (npm:real@^1) is the real package; workspace, portal:, link: and file: dependencies are the project's own and add no package: one on a workspace package of the project (also a name the locks install only as that workspace) is an edge to the workspace's directory. The peer dependencies npm 7 and later install are edges of package-lock.json too, an optional one only when it is installed. A package that installs on some platforms only — the binaries esbuild and its like ship as one optional dependency per platform, all of which a lock file lists — stays in the graph, and its node's platform (Yarn Berry's conditions, or the os, cpu and libc that npm, pnpm and Bun copy into the lock) says where, as os=linux & cpu=x64; the side panel shows it as "installs on". A git dependency (github:owner/repo, git+ssh://…), whether the project or a package needs it, is pinned to the commit its lock file records, which is its version; the repository, without any token its URL holds, is its origin, so the registry is not asked about it (see Findings for what the vulnerability database is asked). One the lock names only a branch or a tag of is not pinned, and the resolution report notes it (git-unpinned).

A Python package that no lock file gives edges for (Pipfile.lock records none) falls back to the installed environment (see Languages). Ecosystems that keep the dependency graph outside the repository — Go modules, NuGet packages no paket.lock or packages.lock.json records (both record the graph, and answer offline), container images, a Composer, Bundler, Mix or R project that commits no lock, pub, whose pubspec.lock is a flat list, rebar3, whose rebar.lock records only a depth, and a Haskell project without cabal's build plan on disk (cabal.project.freeze and stack.yaml.lock list versions only), Terraform registry modules terraform init has not installed, pods no Podfile.lock records, rocks no LuaRocks tree of the repository holds (luarocks.lock is a flat list), CPAN distributions no cpanfile.snapshot records, opam packages no dune.lock/ records (an *.opam.locked is a flat list) and Julia packages no Manifest.toml records, Gleam packages no manifest.toml records, Elm packages the compiler has not installed in ELM_HOME (an application's elm.json lists indirect packages flat), PureScript packages no spago.lock records and spago has not installed into .spago/ (a spago.dhall project's package set is remote), dub packages dub has not fetched onto this machine (dub.selections.json is a flat list), Alire crates no alire.lock records and Alire has not fetched, Maven artifacts of Java, Kotlin, Scala and Clojure builds (Maven, Gradle without its lock files, sbt, tools.deps and Leiningen), Bazel modules (a lock file since Bazel 7.2 records versions only), Puppet modules r10k has not installed, Racket packages (raco keeps no lock file), Wally packages no wally.lock records, Buf Schema Registry modules (buf.lock is a flat list), CUE modules missing from cue's module cache on this machine ($CUE_CACHE_DIR, else cue in the user's cache directory), Swift registry packages SwiftPM has not checked out under .build (Package.resolved is a flat list), Conan packages no Conan 1 lock records (a Conan 2 conan.lock is a flat list), the actions and reusable workflows of other repositories that a GitHub workflow names, and PowerShell Gallery modules — require --online, described below; vcpkg, Bioconductor packages no lock records, Swift packages from git repositories that SwiftPM has not checked out and Terraform modules fetched from git or an archive are not resolved beyond the first level at present. Content a CMake build fetches is not resolved beyond the first level either, nor are Carthage dependencies not checked out into Carthage/Checkouts/ (Cartfile.resolved is flat) and Zig packages Zig has not fetched into zig-pkg/ or its global cache (there is no Zig registry for --online to ask), nor Bazel's WORKSPACE repositories, nor niv and npins sources (their sources.json is flat), nor Crystal shards shards has not installed into lib/ (shard.lock is flat, and shards are git repositories with no index to ask), nor the GitHub, git and HTTP files Paket fetches, nor fpm packages fpm has not fetched into build/dependencies/ (fpm keeps no lock file, and its registry has no dependency API), nor haxelib libraries that neither lix pins nor haxelib installed (lib.haxe.org offers no JSON API to ask). Quicklisp projects that neither Qlot installed into .qlot/ nor ocicl into systems/ need --online too: qlfile.lock and ocicl.csv are flat lists. A git submodule of a Foundry project depends on the submodules of its own .gitmodules when it is checked out; Soldeer packages are followed only where Soldeer installed them into dependencies/ (soldeer.lock is flat, and Soldeer's registry has no dependency data: its API serves each revision as a zip archive, which is not downloaded), nor are nimble packages no nimble.lock records and nothing installed (atlas.lock is flat, and the package list has no dependencies to ask), nor remote Dhall imports (they are not fetched). A Terraform provider depends on nothing.

The side panel presents these as a tree: every row under Depends on and Used by expands into that node's own dependencies, and so on recursively. Nothing is fetched, since the edges are already present in the map, so a row expands immediately; ▸/▾ or the arrow keys expand and collapse it, and the expansion state is preserved when a --watch update redraws the panel. A package that depends on something which in turn depends on it is shown once more, marked ↻, and left collapsed, since lock files do contain cycles and a tree following one would not terminate.

Package indexes

Every external package records the index it comes from. depphunter reads both the index configuration present on this machine and the configuration the repository carries — .npmrc, including @scope:registry; .yarnrc.yml, those of the directories above it and ~/.yarnrc.yml (npmRegistryServer, npmScopes, YARN_NPM_REGISTRY_SERVER); Yarn 1's .yarnrc and ~/.yarnrc; Bun's bunfig.toml and global bunfig ([install] registry, [install.scopes]); pip.conf and a requirements file's --index-url and --extra-index-url; uv's indexes and find-links in pyproject.toml, uv.toml and the user's and system's uv.toml (UV_INDEX, UV_DEFAULT_INDEX, UV_FIND_LINKS), Poetry's sources in pyproject.toml, a Pipfile's and Pipfile.lock's sources, PDM's sources in pyproject.toml and pdm.toml and its own config.toml (PDM_PYPI_URL), each with the packages pinned to one; NuGet.config and the source lines of paket.dependencies and feeds of paket.lock (nuget.org itself and directories aside); a POM's <repositories> and the maven repositories of Gradle build and settings scripts (not those of pluginManagement or buildscript), and the resolvers of an sbt build, other than Maven Central; the mirrors of Maven's settings.xml and the repositories of its active profiles; the repositories of Gradle's init scripts, of the Clojure CLI's user deps.edn, of Leiningen's :user profile and of ~/.sbt/repositories, and COURSIER_REPOSITORIES; .cargo/config.toml; the composer repositories of composer.json and of Composer's own config.json; a Gemfile's source lines (a source ... do block serves only its gems), Gemfile.lock's remotes, ~/.gemrc and Bundler's rubygems.org mirror; a pubspec.yaml's hosted: servers, the servers pubspec.lock resolved from and PUB_HOSTED_URL; HEX_API_URL, HEX_API or the api_url of Hex's hex.config; the repositories of renv.lock (asked in order, each also serving the packages recorded from it), options(repos = ...) in .Rprofile, ~/.Rprofile and R_PROFILE_USER, and RENV_CONFIG_REPOS_OVERRIDE; the repository stanzas and active-repositories of cabal.project and of the configuration cabal reads (CABAL_CONFIG, CABAL_DIR, ~/.cabal/config, ~/.config/cabal/config) other than Hackage itself; a Podfile's source lines (asked in order) and the spec repositories of Podfile.lock (each serving the pods installed from it) other than CocoaPods' own, and the spec repositories cloned in ~/.cocoapods/repos; the registries of SwiftPM's registries.json, the user's and the repository's; the remotes of Conan's remotes.json, in this machine's Conan home and in the home a repository's .conanrc names; the GitHub instances GITHUB_API_URL, GH_HOST and the GitHub CLI's hosts.yml name; the repositories PSResourceGet and PowerShellGet register; the rocks_servers of a project's .luarocks/config-5.x.lua and of the user's configuration (LUAROCKS_CONFIG_5_x, LUAROCKS_CONFIG, $XDG_CONFIG_HOME/luarocks, ~/.luarocks); the registries of DUB_REGISTRY, of dub's settings.json (registryUrls) and of a dub.settings.json, asked before code.dlang.org; the dists a qlfile adds (dist, and the Ultralisp dist of an ultralisp line) and those installed in ~/quicklisp, asked before the Quicklisp dist; the CPAN mirrors cpanm (PERL_CPANM_OPT's --mirror under --mirror-only) and Carton (PERL_CARTON_MIRROR) install from; the repositories of opam's current switch and a dune-workspace's repository stanzas; the indexes alr uses; the registries installed in the Julia depots; the Maven repositories of a deps.edn or bb.edn (:mvn/repos), a project.clj or build.boot (:repositories) and a shadow-cljs.edn other than Maven Central and Clojars; the --registry lines of a .bazelrc and ~/.bazelrc, and of the files their import and try-import lines name, other than the Bazel Central Registry; and GOPROXY — and the side panel names the index each package resolves from. A container image requires no configuration, since ghcr.io/org/app names its registry directly, and neither does a Terraform module: app.terraform.io/acme/vpc/aws names its registry, which is trusted when Terraform's CLI configuration names the host (see Authenticated registries).

Configuration locations

The files named above as ~/... are where each tool keeps its configuration by default. depphunter looks for the machine's configuration, and the credentials kept beside it, where the tool itself would, following the tool's own variables and platform paths:

Tool Where, in the tool's order of precedence
npm npm_config_* variables in any case (npm_config_registry, npm_config_@scope:registry, npm_config_//host/:_authToken; the lower-case spelling wins); the user's npmrc (npm_config_userconfig, else ~/.npmrc); the global npmrc (npm_config_globalconfig, else etc/npmrc under npm_config_prefix; npm's built-in prefix is not guessed)
Yarn YARN_NPM_REGISTRY_SERVER, YARN_NPM_AUTH_TOKEN, _IDENT, _ALWAYS_AUTH; Yarn Berry's file (named YARN_RC_FILENAME, else .yarnrc.yml) in each directory above the analyzed checkout, then ~/.yarnrc.yml, merged key by key, ${VAR} resolved from the environment; Yarn 1's ~/.yarnrc (registries only: Yarn 1 takes credentials from the npmrc)
Bun $XDG_CONFIG_HOME/.bunfig.toml when it exists, else ~/.bunfig.toml, $VAR resolved from the environment
pip PIP_INDEX_URL and PIP_EXTRA_INDEX_URL; the file PIP_CONFIG_FILE names; the user's pip.conf ($XDG_CONFIG_HOME/pip, ~/Library/Application Support/pip on macOS, %APPDATA%\pip\pip.ini on Windows, and the legacy ~/.pip), skipped when PIP_CONFIG_FILE names an existing file; the site-wide /etc/pip.conf, $XDG_CONFIG_DIRS/pip/pip.conf, %ProgramData%\pip\pip.ini. PIP_CONFIG_FILE=/dev/null switches every file off
uv UV_DEFAULT_INDEX, UV_INDEX, UV_FIND_LINKS, UV_INDEX_STRATEGY and the legacy UV_INDEX_URL, UV_EXTRA_INDEX_URL; the file UV_CONFIG_FILE names alone; else the user's uv.toml ($XDG_CONFIG_HOME/uv, ~/.config/uv; %APPDATA%\uv on Windows) and the system's (the first $XDG_CONFIG_DIRS/uv/uv.toml, else /etc/uv/uv.toml; %ProgramData%\uv on Windows). UV_NO_CONFIG reads no uv.toml, the repository's included
Poetry config.toml and auth.toml in POETRY_CONFIG_DIR, else %APPDATA%\pypoetry on Windows, ~/Library/Application Support/pypoetry on macOS, else $XDG_CONFIG_HOME/pypoetry (~/.config/pypoetry); POETRY_REPOSITORIES_<NAME>_URL, POETRY_HTTP_BASIC_<NAME>_USERNAME/_PASSWORD
PDM PDM_PYPI_URL, PDM_PYPI_USERNAME, PDM_PYPI_PASSWORD over config.toml in PDM_CONFIG_FILE, else %LOCALAPPDATA%\pdm\pdm on Windows, ~/Library/Application Support/pdm on macOS, else $XDG_CONFIG_HOME/pdm (~/.config/pdm)
Cargo CARGO_REGISTRIES_<NAME>_INDEX, _TOKEN and _CREDENTIAL_PROVIDER (the name upper-cased, - as _), CARGO_REGISTRY_TOKEN and _CREDENTIAL_PROVIDER for crates.io, CARGO_REGISTRY_GLOBAL_CREDENTIAL_PROVIDERS; config/config.toml and credentials/credentials.toml in CARGO_HOME, else ~/.cargo (the file without an extension when both exist)
Go the environment, else the go env file (GOENV, else go/env in the user configuration directory; GOENV=off for none), for GOPROXY, GOPRIVATE, GONOPROXY, GONOSUMDB and GOAUTH (its netrc and off entries; git and command entries are not run)
containers REGISTRY_AUTH_FILE alone when set; else $XDG_RUNTIME_DIR/containers/auth.json (Linux; ~/.config/containers/auth.json elsewhere), $XDG_CONFIG_HOME/containers/auth.json, and Docker's config.json in DOCKER_CONFIG, else ~/.docker; the first file holding a registry's credential wins, as in containers-auth.json(5)
registries CONTAINERS_REGISTRIES_CONF, else $XDG_CONFIG_HOME/containers/registries.conf (~/.config/...) when it exists, else /etc/containers/registries.conf; then the *.conf of /etc/containers/registries.conf.d and the user's registries.conf.d (only the user's beside the user's file), by name
dockerd daemon.json: the rootless daemon's ($XDG_CONFIG_HOME/docker, ~/.config/docker) when it exists, else /etc/docker; Docker Desktop's ~/.docker on macOS and Windows, else %ProgramData%\docker\config
netrc NETRC; else ~/_netrc on Windows when it exists; else ~/.netrc
NuGet nuget.config in each directory above the analyzed checkout; %APPDATA%\NuGet\NuGet.Config on Windows, else ~/.nuget/NuGet/NuGet.Config and ~/.config/NuGet/NuGet.Config, then the *.config of the config directory beside it; the machine-wide *.config of NuGet\Config under %ProgramFiles(x86)% on Windows, else NUGET_COMMON_APPLICATION_DATA, /Library/Application Support (macOS) or /etc/opt (Linux)
Composer one home, for repositories and credentials alike: COMPOSER_HOME; %APPDATA%\Composer on Windows; else the first that exists of $XDG_CONFIG_HOME/composer (~/.config/composer) and ~/.composer
Bundler BUNDLE_USER_CONFIG; else config in BUNDLE_USER_HOME; else ~/.bundle/config
Maven ~/.m2/settings.xml, then conf/settings.xml under MAVEN_HOME, else M2_HOME (the user's file wins); -s and MAVEN_ARGS are not followed
Gradle the init scripts of GRADLE_USER_HOME, else ~/.gradle: init.gradle(.kts), then init.d/*.gradle(.kts) by name; gradle.properties there for credentials
sbt -Dsbt.repository.config, else repositories in -Dsbt.global.base, else ~/.sbt, those properties and -Dsbt.override.build.repos read from JAVA_OPTS, then SBT_OPTS; credentials in SBT_CREDENTIALS, ~/.sbt/.credentials, ~/.ivy2/.credentials
Coursier COURSIER_REPOSITORIES; COURSIER_CREDENTIALS (inline, or a file: a path or a file: URL, file:///C:/x being C:\x on Windows); else credentials.properties in COURSIER_CONFIG_DIR, %APPDATA%\Coursier\config on Windows, ~/Library/Application Support/Coursier on macOS, else $XDG_CONFIG_HOME/coursier (~/.config/coursier)
Clojure deps.edn in CLJ_CONFIG, else $XDG_CONFIG_HOME/clojure when that is set, else ~/.clojure; Leiningen's profiles.clj in LEIN_HOME, else ~/.lein
Dart pub-tokens.json in %APPDATA%\dart on Windows, ~/Library/Application Support/dart on macOS, else $XDG_CONFIG_HOME/dart (~/.config/dart)
Hex HEX_API_URL, HEX_API, HEX_API_KEY, HEX_REPOS_KEY; hex.config in HEX_HOME, else $XDG_CONFIG_HOME/hex (~/.config/hex) under MIX_XDG=1, else ~/.hex
rebar3 {hex, [{repos, ...}]} of rebar.config in .config/rebar3 under REBAR_GLOBAL_CONFIG_DIR, else the home directory; hex.config there, under REBAR_CACHE_DIR when only that is set
cabal CABAL_CONFIG, else config in CABAL_DIR, else ~/.cabal/config while ~/.cabal exists and the XDG file does not, else $XDG_CONFIG_HOME/cabal/config (~/.config/cabal/config); %APPDATA%\cabal\config on Windows
LuaRocks per Lua version, the file LUAROCKS_CONFIG_5_x, else LUAROCKS_CONFIG, names when it exists; else config-5.x.lua in $XDG_CONFIG_HOME/luarocks (~/.config/luarocks) when it exists, else in ~/.luarocks; %APPDATA%\luarocks on Windows
dub DUB_REGISTRY; settings.json in DUB_HOME, else dub under DPATH, else %APPDATA%\dub on Windows, else ~/.dub; then %ProgramData%\dub on Windows, else /etc/dub and /var/lib/dub; its skipRegistry too
Quicklisp the distinfo-subscription-url of each dist installed in ~/quicklisp/dists
Bazel ~/.bazelrc and the files its import and try-import lines name (not %workspace% ones)
cpanm PERL_CPANM_OPT (its --mirror, --mirror-only and --from), PERL_CARTON_MIRROR
opam OPAMROOT, else %LOCALAPPDATA%\opam on Windows, else ~/.opam: repo/repos-config, the root's config and the current switch's (OPAMSWITCH) switch-config, and the repository copies in repo/
Alire ALIRE_SETTINGS_DIR (Alire 1: ALR_CONFIG), else %USERPROFILE%\.config\alire on Windows, else $XDG_CONFIG_HOME/alire (~/.config/alire): indexes/<name>/index.toml and the checkout beside it
Julia JULIA_DEPOT_PATH (;-separated on Windows, else :; an empty entry is the default depot), else ~/.julia: each depot's registries/<Name>/ and registries/<Name>.toml with the archive it names
r10k /etc/puppetlabs/r10k/r10k.yaml, else /etc/r10k.yaml: forge: baseurl and authorization_token
cue CUE_REGISTRY; logins.json in CUE_CONFIG_DIR, else cue in the user configuration directory
buf BUF_TOKEN; the netrc entries buf registry login writes
CocoaPods CP_REPOS_DIR, else repos below CP_HOME_DIR, else ~/.cocoapods/repos: each spec repository, a git clone by its origin remote or a CDN source by its .url
SwiftPM registries.json in ~/Library/org.swift.swiftpm/configuration on macOS when it is there, else in configuration below $XDG_CONFIG_HOME/swiftpm or ~/.swiftpm; the netrc for swift package-registry login (the keychain is not read)
Conan CONAN_HOME (a leading ~ being the home directory), else ~/.conan2: remotes.json, credentials.json (the environment variables its template names substituted) and extensions/plugins/auth_remote.py (found, not run); CONAN_LOGIN_USERNAME[_<REMOTE>], CONAN_PASSWORD[_<REMOTE>]
GitHub GITHUB_API_URL, GH_HOST, GH_TOKEN, GITHUB_TOKEN, GH_ENTERPRISE_TOKEN, GITHUB_ENTERPRISE_TOKEN; the GitHub CLI's hosts.yml in GH_CONFIG_DIR, else gh below XDG_CONFIG_HOME, else %AppData%\GitHub CLI on Windows, else ~/.config/gh (a token gh keeps in the keyring is not read)
PowerShell PSResourceGet's PSResourceGet/PSResourceRepository.xml in %LOCALAPPDATA% on Windows, ~/Library/Application Support on macOS when it is there, else $XDG_DATA_HOME (~/.local/share); PowerShellGet 2's PSRepositories.xml in %LOCALAPPDATA%\Microsoft\Windows\PowerShell\PowerShellGet on Windows, else powershell/PowerShellGet below $XDG_CACHE_HOME (~/.cache)

Nothing is read from the repository through these variables' defaults; they only say where this machine's own files are.

The two sources are not treated alike. An index named by this machine's own configuration is trusted. One that appears only in the repository is recorded and marked ⚠ index, because a repository directing a package manager at an index that nothing here configures is the form a dependency-confusion attack takes. No request is ever made to such an index, unless it is vouched for with --trust-index.

Which index is asked

Package managers differ in how a configured index relates to the public one, and depphunter follows each of them. A source either replaces the public default or is asked beside it:

Ecosystem replaces the public default asked beside it
PyPI index-url, PIP_INDEX_URL, -i; a uv index with default = true (a repository's before the user's uv.toml's); a PDM source named pypi; Poetry's primary sources, asked in order, PyPI only when one of them is it (a declared PyPI of any priority drops the implicit one) extra-index-url, PIP_EXTRA_INDEX_URL; any other uv index or find-links location; any other PDM source; after the primary index: a supplemental (or legacy secondary) Poetry source, the PDM sources after PyPI under respect-source-order
Maven a settings.xml mirror of central; a mirror of * or external:* (or one without mirrorOf), which stands in for every repository; a settings profile or Clojure repository with the id central; COURSIER_REPOSITORIES and, under -Dsbt.override.build.repos=true, ~/.sbt/repositories, lists asked in order a POM's <repositories>, Gradle's maven { url … } (a build's or an init script's), sbt's resolvers, Clojure's :mvn/repos and :repositories (a project's or the user's), an active settings profile's repositories, ~/.sbt/repositories, a mirror of any other repository; Clojars after Central for a Clojure project
Composer nothing: "packagist.org": false switches Packagist off every composer repository
NuGet nothing: nuget.org is off when the merged configuration leaves it out (a <clear/> with no closer entry for it, or it disabled), or when paket.dependencies lists sources and none is nuget.org every enabled feed; every Paket source
Go GOPROXY: its proxies are asked in order, and proxy.golang.org only if it is on the list —
CRAN R's repos option (options(repos = ...) of an R profile), renv.lock's repositories and RENV_CONFIG_REPOS_OVERRIDE: each list asked in order, CRAN only when it is on it (as @CRAN@ or a mirror) the literal repositories of a repos value that extends one it does not spell out, c(getOption("repos"), ...)
Hackage a mirror under Hackage's own name; cabal's active-repositories, whose repositories are asked last to first; a cabal configuration listing repositories without Hackage leaves it out every other repository stanza of cabal's configuration and of cabal.project
LuaRocks rocks_servers, asked in order (a group's mirrors each after any failure of the one before), luarocks.org only when it is on the list —
CPAN cpanm's --mirror list under --mirror-only or --from, asked in order, MetaCPAN only when a public CPAN mirror is on it PERL_CARTON_MIRROR
opam the repositories of the opam switch, asked in its order of priority, opam-repository only when it is one of them; a dune-workspace lock_dir's repositories, likewise a dune-workspace's repository stanzas when no lock_dir lists any
Alire alr's indexes, asked by priority, the community index only when it is one of them —
CocoaPods a Podfile's source lines, asked in its order, the CDN only when it is one of them (or none is listed) —
SwiftPM nothing: there is no public registry — (a package is asked of the one registry registries.json maps its scope to, else of the default registry)
Conan remotes.json's remotes, asked in its order, ConanCenter only when it is one of them (or there is no remotes.json); a remote's allowed_packages limit what it is asked for —
GitHub Actions nothing: api.github.com is always asked last the GitHub instances of GITHUB_API_URL, GH_HOST and hosts.yml, asked first
PowerShell Gallery the repositories PSResourceGet registers, asked by priority, and PowerShellGet 2's; the Gallery only when one of them is it (or nothing is registered) — (an install naming -Repository is asked of that repository alone)
crates.io [source.crates-io] replace-with (followed to the end of the chain) —
npm and every other ecosystem the first unscoped source found (npm's registry, then Yarn's npmRegistryServer, ~/.yarnrc's registry and Bun's [install] registry) —

A package is asked of the sources beside the public default first, in the order they were found (this machine's, then the repository's), and of the public default, or what replaces it, last. The next index is asked only when one says it does not have the package: a 404 or 410, or an answer that does not list it. An index that fails in any other way — unreachable, 401, 500 — ends the question, since the next index's package of that name may not be the same package; the one exception is a GOPROXY entry followed by |, which the go command also passes over on any failure. GOPROXY entries after direct or off are not reached, and GOPROXY=direct or GOPROXY=off leaves no proxy to ask.

A module the go command fetches directly from version control — one matching GONOPROXY or GOPRIVATE, or reached through direct — is not asked at all. Reading its go.mod from the forge's raw-file URL would name a module this machine's own configuration declares private, with its version, to a host outside that configuration, which is what GOPRIVATE is there to prevent. Whether a proxy is sent the netrc's credential follows GOAUTH: unset or listing netrc, it is sent; off, or a list of only git <dir> and command entries, and it is not. Those two forms run a program for the credential, which depphunter does not do.

A container image is asked where this machine's container tools pull it from. The [[registry]] of registries.conf whose prefix matches the most of the image's name (docker.io/library/nginx for nginx) rewrites it: its [[registry.mirror]] entries are asked first, in order — those pull-from-mirror or mirror-by-digest-only limit to digests only for a digest, to tags only for a tag — and its location last, each with the matched prefix replaced by its own location. A mirror that fails in any way is passed over, as Podman and Docker pass over one. A blocked = true registry is never asked, and the report says so. A Docker Hub image no [[registry]] matches is asked of the Docker daemon's registry-mirrors first, then of Docker Hub. The map attributes the image to its registry either way; the report names the mirror that answered. unqualified-search-registries, short-name-mode and short-name aliases are not applied, since the map names every Docker Hub image the short way however it was written, and Docker itself searches no list. A location is asked over https even when insecure = true.

A CPAN mirror — a DarkPAN, a Pinto or OrePAN2 repository, a minicpan directory — is read from its modules/02packages.details.txt.gz, once: a distribution it does not list is asked of the next index, as cpanm does. The archives are not downloaded, so a distribution it lists is given the dependencies of the same release on MetaCPAN (by the author its path names); one MetaCPAN does not describe is shown without dependencies, with a no-release note. A distribution a private pattern covers is not named to MetaCPAN, which is why a DarkPAN's own distributions belong under --private patterns. opam and alr keep a copy of every repository and index they use (~/.opam/repo/<name> or its .tar.gz, ~/.config/alire/indexes/<name>/repo), and that copy is read instead of the network: it lists a package's versions, which a file server cannot, and it holds private repositories whatever serves them. A range is answered by the newest version it admits — in opam's version order, or Alire's semantic versioning — without either tool's solver. Without a copy, opam-repository and the Alire community index are listed through GitHub's contents API (60 requests an hour without a credential for api.github.com in the netrc); another repository served over HTTP is asked only about pinned versions, and one only git serves is not read (a no-copy note says so).

Julia's registries are read the same way: every registry installed in a depot (JULIA_DEPOT_PATH, else ~/.julia) — a git checkout in registries/<Name>/, or the archive a Pkg server served (registries/<Name>.toml naming <Name>.tar.gz, read once) — is read from that copy, whatever host it came from, and General's copy is read in place of its files on GitHub. As in Pkg, a package is looked up by its UUID (from Project.toml and Manifest.toml, and each registry's Deps.toml) in every installed registry that lists it, so another registry's package of the same name is not mistaken for it; one no registry lists is General's. With registries installed and General not among them, General is not asked. Without any installed registry, General's files are read from GitHub. JULIA_PKG_SERVER is not asked: a Pkg server serves whole registries as archives by tree hash (/registries, /registry/<uuid>/<hash>), and the copy it installed in the depot is what is read.

CocoaPods' spec repositories are read the same way. Each git clone in CocoaPods' repos directory (pod repo add; CP_REPOS_DIR, else ~/.cocoapods/repos) is read in place of the repository its origin remote names — compared as CocoaPods compares them, without case, .git or a trailing slash — so a company's private spec repository answers from the disk, whatever host serves it, and a clone of the trunk repository answers before the CDN is asked; the CDN source's own directory (~/.cocoapods/repos/trunk) answers with the version lists and podspecs CocoaPods already downloaded. A clone is read as CocoaPods lays it out: below Specs/ or its root, sharded as its CocoaPods-version.yml says, a .podspec.json or a Ruby .podspec (whose dependency lines are read without running Ruby). A clone is no index of its own: the pods are asked of the repositories the Podfile lists, in its order, and a repository the project names that this machine has a clone of is trusted, since this machine uses it already. A git spec repository without a clone is not read (a no-copy note says so) and the next one is asked.

A Swift registry package (.package(id: "scope.name")) is asked of the registry SwiftPM's registries.json maps its scope to, else of the default registry ([default]) — the repository's file (in .swiftpm/configuration beside its Package.swift) over the user's, scope by scope, as SwiftPM merges them. A registry only the repository's file names is marked ⚠ index and not asked until it is vouched for, whatever credential this machine holds for its host; one the user's file names too is trusted. A package named by its repository's URL is asked of no registry.

A Conan package is asked of the remotes of Conan 2's remotes.json, in the file's order, as Conan asks them: this machine's (in CONAN_HOME, else ~/.conan2; ConanCenter alone when there is no file), then those of the home a repository's .conanrc names (conan_home=./.conan2, read from the disk, the .conanrc nearest the conanfile up to the repository's root). A disabled remote and a local-recipes-index folder are not asked, and a remote's allowed_packages patterns limit it to the references they match. The .conanrc's home is the repository's choice wherever it is: its remotes are marked ⚠ index and not asked until vouched for, unless this machine's own remotes.json lists the same remote, and its credentials.json is not read. ConanCenter (center2.conan.io, and the older center.conan.io) is the public index, never asked about a private package. A version range is resolved as Conan resolves it, against the remote's search, and a package the repository's conan.lock pins is asked about, and answered, at the lock's version and recipe revision.

A GitHub action or reusable workflow (owner/repo[/path]@ref) names no host: an Enterprise Server runs the one it holds and, with GitHub Connect, falls back to github.com's. It is asked of the GitHub instances this machine works with first — the one GITHUB_API_URL names (on a runner of that instance), the one GH_HOST names and every other host of the GitHub CLI's hosts.yml, each at its REST API (<host>/api/v3, api.<name>.ghe.com) — and then of github.com; an organization's own action (--private 'actions:acme/*') is never named to github.com. A repository cannot name an instance.

A PowerShell module is asked of the repositories this machine registers, as PowerShell's installers ask them: PSResourceGet's (PSResourceRepository.xml) by priority, then by name, then PowerShellGet 2's (PSRepositories.xml) in their order; the Gallery only when one of them is it, or when neither file exists. A local repository and a container registry are not asked. An Install-Module or Install-PSResource naming -Repository, or a PSDepend dependency naming its Repository, is asked of that repository alone, by the name it was registered under, and so are the module's dependencies, as PowerShellGet installs them from the same repository.

Five kinds of source are authoritative and have no fallback: a scoped source that covers the package (an npm @scope:registry, a Gemfile source block, a pubspec's hosted: server, a Python package pinned to an index by uv's [tool.uv.sources], Poetry's source =, Pipenv's index =, or by PDM's include_packages, whose matching sources are all asked, in order), the NuGet feeds packageSourceMapping maps the package to, a Cargo alternative registry, a private Hex organization (organization: "acme" or repo: "hexpm:acme" in mix.exs, "hexpm:acme" in mix.lock), and the host an image (with the mirrors this machine configures for it, below) or Terraform module names. A package missing from one of them is not looked for on the public index, which is exactly where a dependency-confusion attack would plant it. A Cargo registry of [registries.<name>] serves only the crates that declare it — registry = "<name>" in Cargo.toml, or its index as Cargo.lock's source — and every other crate stays with crates.io.

NuGet's configuration files are merged as NuGet merges them: the machine-wide files, the additional user files (~/.nuget/NuGet/config/*.config), the user's NuGet.Config, the nuget.config files of the directories above the analyzed one, then the repository's nuget.config files (a deeper one over a shallower one), a closer file's entry replacing the same key and a <clear/> dropping everything before it — so a repository's <clear/> drops this machine's feeds as well. A feed <disabledPackageSources> disables is not asked. Under <packageSourceMapping>, a package is asked only of the feeds mapped to its most specific pattern — an exact id over Contoso.*, a longer prefix over a shorter one, * last — so Contoso.Billing mapped to the company feed is never named to nuget.org, even when the feed lacks it. A package no pattern covers is asked of every feed and nuget.org as usual, where NuGet itself would refuse it; Paket's feeds are not mapped.

Python's tools are read in their own terms. A repository's uv indexes are asked before those of the user's and the system's uv.toml, and its default index wins over theirs, as uv reads a project's configuration first; uv's variables still come before both. A uv.toml beside a pyproject.toml replaces its [tool.uv] indexes, as in uv, while its [tool.uv.sources] still pins packages, as uv resolves the names: to an index UV_INDEX or UV_DEFAULT_INDEX names, else to the pyproject.toml's own [[tool.uv.index]], never to a uv.toml's; UV_NO_CONFIG and UV_CONFIG_FILE leave a repository's uv.toml unread. A flat index (an index with format = "flat", find-links, UV_FIND_LINKS) is a page or a directory listing distribution files: its file names give the versions, and a file's PEP 658 .metadata its requirements, or, for a directory this machine names, the METADATA of a wheel in it. A repository's flat index is recorded only when it is a URL, untrusted like any repository index. Poetry's primary sources (after any of priority default) are asked in the order written, and PyPI only when one of them is a source named PyPI; its supplemental (and legacy secondary) sources are asked after them, or after PyPI when no source is primary, and a source named PyPI of any priority drops the implicit PyPI, as Poetry does. A Pipfile's first source replaces PyPI and its others are asked beside it, as Pipenv passes them to pip. A PDM source named pypi replaces PyPI; a package some sources' include_packages patterns match is asked of those sources alone, and a source is never asked for a package its exclude_packages patterns match (nor PyPI, when the pypi source excludes it). Under PDM's respect-source-order its sources are asked in order around PyPI (after it when no source is named pypi). uv under index-strategy unsafe-best-match or with find-links, and PDM without respect-source-order, merge the versions all their indexes have: depphunter still asks the indexes in order and takes the first that has the package, and --explain says so once (merged). Poetry's config.toml repositories are where it publishes, not sources. When several of this machine's tools name a replacement for PyPI, pip's comes first, then uv's, then PDM's.

R, cabal and LuaRocks ask every repository they are given. R's repos option, renv.lock's repositories and RENV_CONFIG_REPOS_OVERRIDE are lists asked in order, CRAN (written as a mirror or as @CRAN@) only where it is on the list; R keeps the highest version any repository has, while depphunter takes the first repository that has the package. cabal combines the repositories of its configuration and of cabal.project, so a company repository is asked before Hackage, and a configuration that lists repositories without Hackage leaves Hackage out; active-repositories (:rest, :none, :override) chooses the repositories and is searched last to first, as cabal does. A rocks_servers list replaces LuaRocks' default: its servers are asked in order, luarocks.org only when listed. Paket asks only the sources paket.dependencies lists, so nuget.org is not asked when they leave it out.

A source asked beside the public default does not decide where a package is drawn from until something is asked: a repository's --extra-index-url no longer marks every PyPI package ⚠ index. The repository's index is still never queried unless vouched for; the package is asked of the indexes this machine may query, and only one that none of them has is attributed to the repository's index and marked. With --online the map moves each package to the index that actually answered.

--online permits depphunter to query the trusted indexes for dependencies the repository does not record, which is how --resolve-depth reaches the ecosystems whose graph is held outside the repository:

Ecosystem asked for answer
Go <proxy>/<module>/@v/<version>.mod its direct (non-// indirect) require entries
npm <registry>/<package>/<version>, or /latest when unpinned its dependencies
PyPI <host>/pypi/<name>/<version>/json, or /pypi/<name>/json when unpinned; elsewhere, if that is 404, the Simple API page <index>/<name>/ and a file's PEP 658 <file>.metadata requires_dist (Requires-Dist), excluding extras
crates.io <index>/<se>/<rd>/<name>, sparse index its normal deps, excluding optional ones
NuGet <feed>/<id>/<version>/<id>.nuspec <dependencies>, both flat and by group
OCI the manifest, then its config blob the base image it was built on
Composer <repository>/p2/<vendor>/<name>.json (metadata-url elsewhere) the version's require, excluding the platform
RubyGems <server>/info/<name>, the compact index Bundler reads the version's runtime dependencies
pub <server>/api/packages/<name>, the package API pub reads the version's (or latest's) dependencies
Hex <api>/packages/<name>, then /releases/<version> (or latest stable); <api>/repos/<org>/packages/<name> for a private organization's package its requirements, excluding optional ones
CRAN crandb's /<name>/<version> (or current); src/contrib/PACKAGES elsewhere Depends, Imports and LinkingTo, without R's base packages
Bioconductor <bioconductor.org/packages>/<release>/bioc/src/contrib/PACKAGES, the release renv.lock records (Bioconductor.Version), else release (read once) Depends, Imports and LinkingTo; one the same PACKAGES lacks is CRAN's
Hackage <server>/package/<name>/preferred, then /package/<name>-<version>/<name>.cabal (or newest) its libraries' build-depends, without GHC's own packages
Terraform modules <modules.v1>/<namespace>/<name>/<provider>/versions (service discovery off the public registry) the providers and registry modules of the version asked for, or the newest its constraint allows
CocoaPods <cdn>/Specs/<a>/<b>/<c>/<pod>/<version>/<pod>.podspec.json (newest: the shard's version list); a clone in ~/.cocoapods/repos instead (any host; .podspec.json or .podspec) its and its default subspecs' dependencies
SwiftPM registry <registry>/<scope>/<name> for the releases, then <registry>/<scope>/<name>/<version>/Package.swift (the registry registries.json maps the scope to) its .package(id:) and .package(url:) dependencies
Conan <remote>/v2/conans/<name>/<version>/<user>/<channel>/latest, then .../revisions/<revision>/files/conanfile.py; a range from <remote>/v2/conans/search?q=<name>/* the recipe's requires, pinned as conan.lock pins them
GitHub Actions <api>/repos/<owner>/<repo>/contents/<path>/action.yml?ref=<ref> (then action.yaml; a reusable workflow's own file), raw.githubusercontent.com for github.com without a token a composite action's steps, a Docker action's image (or its Dockerfile's FROM), a reusable workflow's jobs
PowerShell Gallery <repository>/Packages(Id='<name>',Version='<version>'), else <repository>/FindPackagesById()?id='<name>' (NuGet v2 OData; a NuGet v3 feed as NuGet's) its Dependencies (the manifest's RequiredModules)
LuaRocks <server>/<rock>-<version>.rockspec, versions from <server>/manifest-5.1.zip (read once) its run-time dependencies, without lua
CPAN MetaCPAN's <api>/v1/release/<AUTHOR>/<name> (as cpanfile.snapshot records) or /_search for a pinned version, else /v1/release/<distribution>; /v1/module/<module> per dependency; a mirror's 02packages first its run-time requirements as distributions, without perl's own modules
opam <repository>/packages/<name>/<name>.<version>/opam, from opam's copy of the repository, else over HTTP; for a range, the newest version admitted, listed by the copy (opam-repository: GitHub's contents API) its depends, without the compiler and what only tests or documentation need
Julia <registry>/<L>/<Name>/Versions.toml, then Deps.toml and Compat.toml, from a depot's copy of the registry (any host), else over HTTP (General) the dependencies of the pinned or newest admitted release, with their compat ranges, without julia
Maven (group:artifact) <repository>/<group path>/<artifact>/<version>/<artifact>-<version>.pom and its parents, the version from maven-metadata.xml when unpinned; Clojars after Central for a Clojure project its compile and runtime dependencies, excluding optional ones
Bazel modules <registry>/modules/<name>/<version>/MODULE.bazel, the newest version not yanked from metadata.json when unversioned (the Bazel Central Registry, or a .bazelrc --registry) its bazel_deps, excluding dev dependencies
Elm <site>/packages/<author>/<name>/<version>/elm.json, the newest release a range admits from releases.json when unversioned (package.elm-lang.org) its dependencies as ranges, excluding test dependencies
PureScript <owner>/registry-index/main/<shard>/<name> (a JSON manifest per line), the newest version a range admits from <owner>/registry/main/metadata/<name>.json when unversioned (the registry on raw.githubusercontent.com) its dependencies as ranges
dub <registry>/api/packages/<name>/<version>/info, the newest release a specification admits from <registry>/api/packages/<name>/info when not exact (DUB_REGISTRY and the settings' registryUrls first, then code.dlang.org) its, its sub-packages' and its default configuration's dependencies, not optional or path ones
Alire crates <index>/index/<first two letters>/<crate>/<crate>-<version>.toml, from alr's checkout, else over HTTP (alire-index's stable-1.4.0); for a range, the newest release admitted, listed likewise (see opam) its depends-on, every case(...) alternative
Quicklisp the dist's distinfo (quicklisp.txt, or <dist>/<version>/distinfo.txt for a dist version), then the systems.txt it names (read once); a qlfile's or ~/quicklisp's other dists first the dependencies of the project's own systems, each named by its project, without ASDF, UIOP and SBCL's contribs
Puppet Forge <api>/v3/releases/<slug>-<version>, the newest release a range admits from <api>/v3/modules/<slug> when not exact (forgeapi.puppet.com, a Puppetfile's forge, r10k's forge.baseurl) its metadata.json dependencies, by slug, with their requirements
Racket <catalog>/pkg/<name>, the entry's dependencies (pkgs.racket-lang.org) its dependencies, base as the base collections, #:version a minimum
Wally <scope>/<name> in the registry's GitHub repository (a manifest per line; config.json's fallback registries when missing), from the registry wally.toml names its dependencies and server-dependencies, each from the registry its manifest names
Buf Schema Registry GraphService/GetGraph, then ModuleService/GetModules and OwnerService/GetOwners (Connect JSON), at the registry the module name carries its direct dependencies, each pinned by its commit, not another registry's
CUE modules the manifest <registry>/v2/<module>/manifests/<version>, then its module file blob (registry.cue.works, or where CUE_REGISTRY routes the module) its deps, each at the version it names

A container image has no dependency list. What it has is the image it was built on, which is the source of its unpatched vulnerabilities, and that is what is followed: the manifest — one platform's, where the manifest is a multi-platform index — and then the small config blob it references, read for org.opencontainers.image.base.name in the manifest's annotations or the image's labels, for an image named in a pipeline, a Dockerfile or a Compose file alike. No layers are downloaded. A registry requiring a pull token is given the opportunity to say so, and the token endpoint it names is followed only over HTTPS, or back to the registry's own host. An identity token this machine holds for the registry (identitytoken, as docker login stores an Azure Container Registry refresh token, or a credential helper's <token> answer) is exchanged there for the pull token with the OAuth2 refresh-token grant — only at a token endpoint on the registry's own host, over HTTPS.

A private PyPI index that serves only pip's Simple API — GitLab, AWS CodeArtifact, Azure Artifacts, Google Artifact Registry, devpi, a plain Nexus or Artifactory repository — answers 404 at the JSON API, so its project page is read instead (PEP 691 JSON, or PEP 503 HTML), with the release taken from the file names: the pinned version, or the newest that is neither yanked nor a pre-release. Its dependencies are the Requires-Dist of the metadata file the index keeps beside a wheel or source archive (PEP 658), checked against the page's hash. A release whose files advertise no metadata gets no answer, and the report says so: archives are not downloaded. File URLs are taken relative to the page, and each request carries the credential this machine holds for its own URL — the index's for a file below the index's path, none for a file on a host or path it has none for.

Every Maven package on the map names its artifact (com.google.guava:guava, cheshire:cheshire), so its POM is read, whichever plugin placed it there; only a Bazel hub target that no artifact list names is not asked about. Clojars, where Clojure's libraries are published, is asked after Maven Central whenever the repository has a Clojure manifest, as Leiningen and tools.deps do without being told to (not when a mirror of * stands in for both).

Lock files take precedence: an index is queried only where the repository is silent, and an entire level of the walk is queried at once rather than one package at a time. Answers are cached for one day under the cache directory.

Authenticated registries

An index behind authentication is read like any other, provided this machine is already configured for it. Credentials are taken from the user's own files and environment — never from the repository — and each is sent to the host it was written for and to no other.

Source Holds
npm's user and global npmrc, npm_config_//<host>/:<field> _authToken, _auth, and username with _password, per registry host and path
this machine's .yarnrc.yml files, YARN_NPM_AUTH_* Yarn Berry's npmAuthToken and npmAuthIdent: top level, npmScopes, npmRegistries
Bun's global bunfig token, or username and password, of [install] registry and [install.scopes]
~/.netrc (NETRC; ~/_netrc on Windows) the machine/login/password triples git, curl, Go (unless GOAUTH says not) and pip read
Maven's settings.xml (~/.m2, MAVEN_HOME) each <server>, matched to a <mirror>, profile <repository> or user deps.edn repository
this machine's NuGet.Config and nuget.config files <packageSourceCredentials> (ClearTextPassword), matched to its enabled <packageSources> entry
NuGetPackageSourceCredentials_<source> Username=...;Password=... for a source those files name, over its file entry
VSS_NUGET_EXTERNAL_FEED_ENDPOINTS the Azure Artifacts credential provider's endpoint passwords
%NAME% in a repository nuget.config or Paket source a pipeline secret for a repository feed, only when vouched for (below)
${NAME} in a repository .yarnrc.yml or bunfig.toml a pipeline secret for a repository registry, only when vouched for (below)
${NAME} in a repository Pipfile or PDM source URL a pipeline secret for a repository index, only when vouched for (below)
uv's UV_INDEX_<NAME>_USERNAME, _PASSWORD for the index of that name in uv.toml, UV_INDEX or UV_DEFAULT_INDEX
Poetry's auth.toml and config.toml [http-basic.<name>] for its repository <name>; POETRY_HTTP_BASIC_<NAME>_* over them
PDM's config.toml, PDM_PYPI_USERNAME/_PASSWORD username and password of [pypi] and [pypi.<name>], each for its URL
~/.docker/config.json (DOCKER_CONFIG) stored auths and identitytokens, and the helpers of credsStore and credHelpers
containers/auth.json, REGISTRY_AUTH_FILE the same, for Podman and Skopeo
credentials.toml in CARGO_HOME (~/.cargo) a token per registry, matched to its index through config.toml there (legacy credentials and config too)
CARGO_REGISTRIES_<NAME>_TOKEN, CARGO_REGISTRY_TOKEN the same token supplied by a pipeline instead, over the files
[registries.<name>] token in Cargo's config.toml a token kept beside the index; credentials.toml and the variables win
~/.terraformrc, ~/.tofurc, TF_CLI_CONFIG_FILE Terraform's and OpenTofu's credentials "<host>" tokens; a host block names a registry without one
~/.terraform.d/credentials.tfrc.json the tokens terraform login stores (OpenTofu's under ~/.config/opentofu)
TF_TOKEN_<host> a token supplied by a pipeline, for HCP Terraform and the hosts named above
auth.json in Composer's home http-basic, bearer, gitlab-token, gitlab-oauth, github-oauth, per host
COMPOSER_AUTH the same keys supplied by a pipeline, merged host by host over auth.json
~/.bundle/config Bundler's BUNDLE_<HOST> credentials (user:password, or a token), per host or source URL
BUNDLE_<HOST> the same credential supplied by a pipeline, over the file's
~/.sbt/.credentials, SBT_CREDENTIALS sbt's host, user and password, whatever the realm; ~/.ivy2/.credentials too
Coursier's credentials.properties, COURSIER_CREDENTIALS <name>.host, .username and .password; inline host(realm) user:password lines
gradle.properties in GRADLE_USER_HOME <name>Username and <name>Password for an init script's maven { name = "<name>" … }
pub-tokens.json in Dart's configuration directory dart pub token add tokens (or the variable an env entry names), per hosted URL
HEX_API_KEY, hex.config (HEX_HOME, ~/.hex) the Hex user key, api_key or OAuth token; each hexpm:<org> auth_key; HEX_REPOS_KEY
rebar3's hex.config (~/.config/rebar3) hexpm api_key, $oauth token; each hexpm:<org> api_key and repo_key
BUF_TOKEN, the netrc entry buf registry login writes a Buf Schema Registry token per host (a bare BUF_TOKEN for buf.build only)
cue's logins.json (CUE_CONFIG_DIR) the tokens cue login stores, per registry host, over Docker's
r10k's r10k.yaml forge: authorization_token, for the paths of forge: baseurl
the netrc entry swift package-registry login writes a registry's token (Bearer, as the user's registries.json says) or user and password
Conan's credentials.json, CONAN_LOGIN_USERNAME a remote's user and password, for its token (below)
GH_TOKEN, GITHUB_TOKEN, gh's hosts.yml a GitHub host's token, for its REST API alone (below)
the index URL itself https://user:password@host/simple, as a private pip or Cargo mirror is set

Between them these cover Nexus, Artifactory, Azure Artifacts, ProGet, GitHub Packages, Harbor, GHCR, a private crate registry, a private Terraform registry, Private Packagist, Satis, Repman and GitLab's Composer registry, Gemfury and the commercial gem servers Bundler is pointed at, private pub servers, Hex organizations, a private Puppet Forge, Buf Schema Registry, CUE registry or Swift package registry. What SwiftPM keeps in the macOS keychain (its default there) is not read: log in with --disable-keychain to keep the credential in the netrc.

A Conan remote is asked without a login first, as Conan asks it. When it answers 401, the login Conan would use for it — the entry of the home's credentials.json for the remote's name (a Jinja2 template's os.getenv("NAME") substituted), else CONAN_LOGIN_USERNAME_<REMOTE> and CONAN_PASSWORD_<REMOTE> (the name upper case, - as _), else CONAN_LOGIN_USERNAME and CONAN_PASSWORD — goes as Basic credentials to the remote's /v2/users/authenticate alone, over https or on this machine, and the token it answers with is sent as a Bearer token from then on. Conan's auth_remote.py plugin is a program and is not run (--explain notes it, helper-not-run, when a remote wants a login this machine does not hold), and the tokens conan remote login keeps in the home's .conan.db database are not read.

A GitHub REST API gets the token the GitHub CLI would send it, and no other host does: github.com's API GH_TOKEN, else GITHUB_TOKEN, else the oauth_token of github.com in hosts.yml; an Enterprise Server's GH_ENTERPRISE_TOKEN, else GITHUB_ENTERPRISE_TOKEN, else its own entry of hosts.yml — on a runner of that instance (GITHUB_API_URL) its GITHUB_TOKEN, which then does not go to github.com — sent to its /api/v3/ paths alone. Without a token for api.github.com, a github.com action's files are read from raw.githubusercontent.com, which serves public repositories without the API's limit of 60 requests an hour (--explain notes the limit, forbidden, when the API refuses). A token gh keeps in the system's keyring, its default, is not read, and gh auth token is not run. PowerShell repositories are sent the credentials this machine holds for their hosts; the SecretManagement vault entries a registration names are not read.

Each file is looked for where its tool looks for it; see Configuration locations.

A Cargo token is sent as Cargo sends it: the whole Authorization header, exactly as written. A registry that wants a scheme has the token written with it (Artifactory's Bearer <token>); crates.io and most others take it bare. Each named registry's token goes to its own index path only, so registries sharing a host keep their own tokens, and it is sent whether or not the registry's config.json says auth-required. crates.io's token goes to crates.io alone: never to its index, which Cargo reads anonymously, nor to a registry replacing it. A registry whose credential-provider (or, without one, registry.global-credential-providers) leaves out cargo:token keeps its credential in a keychain or a program, which depphunter does not run: its plaintext token is skipped.

Bazel's credential helpers (--credential_helper=[<scope>=]<helper> in a .bazelrc or ~/.bazelrc) are programs, and are not run either. A registry a helper covers is asked with the netrc's credential for its host, if any, and --explain says so (helper-not-run).

A pub token is the one dart pub token add stored in pub-tokens.json, in Dart's configuration directory: the entry's token, or the variable its env names, as a pipeline sets it. It is sent as a Bearer token to the URLs under the hosted URL it was added for, as pub sends it — the whole host for a server at its root, only its path for one under a path — and nowhere else.

A private Hex organization's package is asked of the organization's part of the Hex API, <api>/repos/<org>/packages/<name>, and never of hex.pm's public packages, where a package of the same name would be someone else's. The key sent there is Hex's own: HEX_API_KEY, else the api_key of hex.config, else the OAuth token mix hex.user auth stored while it has not expired — a user's, which reaches every organization the user belongs to; without one, the auth_key mix hex.organization auth <org> --key KEY stored for hexpm:<org>, for that organization alone, and HEX_REPOS_KEY for the others. A key goes out as the whole Authorization header and a token as a Bearer token, only under <api>/repos/; public packages are asked without either. Without a key for the organization nothing is asked, and the resolution report says the key is missing. An organization key generated without --key carries only the repository:<org> permission, which hex.pm's API refuses: use a user key, or one generated with --permission api:read. The requirements of an organization's package are asked of the organization too, since the API does not say which repository each is in; mix.lock, which does, answers first. A repository with a URL of its own (a mini_repo, HEX_MIRROR) serves Hex's protobuf registry, not the API, and is not asked; nor are the encrypted keys of older Hex versions read.

rebar3 records no repository for a package — not in rebar.config's deps, not in rebar.lock — and asks the repositories its configuration names in order: {hex, [{repos, [#{name => <<"hexpm:acme">>}]}]} in the project's rebar.config, then in the global ~/.config/rebar3/rebar.config, then hex.pm's public packages, unless the first entry is {repos, replace, [...]}. A rebar3 project's Hex packages are asked in that same order, each repository only when the ones before it lack the package: one found in an organization is never named to the public side, while one no organization has is looked for on hex.pm, as rebar3 would fetch it from there. The question stops at an organization this machine has no key for (the report says so) and at a repository with no API. Keys also come from rebar3's ~/.config/rebar3/hex.config: a repository's own api_key first, hexpm's api_key or the $oauth token of rebar3 hex user auth as the user's (after Mix's), and each hexpm:<org> repo_key that rebar3 hex organization auth stores for its organization alone.

In ~/.npmrc, settings.xml and NuGet.Config, a value that is exactly ${NAME}, ${env.NAME} or %NAME% is read from the environment, so a password may be held there. An encrypted password — Maven's {...} form, NuGet's Windows-encrypted form — is left untouched: it cannot be decrypted here, and transmitting the ciphertext would yield only a 401.

A credential written into an index URL is removed from that URL before it is recorded. The index a package resolves from is drawn on the map, named in the side panel and written into every export, and the HTML export is a file this document recommends sharing. Only a URL supplied by this machine's own configuration contributes a credential; one supplied by the repository is stripped and discarded, since a repository able to supply a credential would also be choosing where it is sent.

A pipeline usually keeps the repository's nuget.config or paket.dependencies naming the company feed and supplies the secret in a variable: a password: "%NAME%" on a Paket source line, a ClearTextPassword of %NAME%, or a NuGetPackageSourceCredentials_<source> variable for a source only the repository defines. The secret is this machine's but the address is the repository's, so it is sent only to a feed on the host of a NuGet source this machine configures, or one vouched for with --trust-index; a password written out in the repository is discarded. A NuGet credential for pkgs.dev.azure.com, which every Azure DevOps organization shares, serves only its organization's path. NuGet's Windows-encrypted Password and Paket's encrypted credential store (paket config add-credentials) are not read.

The same holds for Yarn and Bun: a repository .yarnrc.yml whose npmAuthToken or npmAuthIdent is exactly ${NAME} (or ${NAME:-x} with NAME set), and a repository bunfig.toml whose token or password is exactly $NAME or ${NAME}, get the variable's value only for a registry on the host of one this machine's npm, Yarn or Bun configuration names, or one vouched for with --trust-index. A token or password written out, one only a fallback fills, and a user:password in a repository registry URL are discarded; a repository registry URL made of a variable is not recorded.

Python's tools name a credential after its index. uv takes UV_INDEX_<NAME>_USERNAME and UV_INDEX_<NAME>_PASSWORD (the name upper-cased, anything but a letter or digit as _), Poetry the [http-basic.<name>] of its auth.toml or config.toml and POETRY_HTTP_BASIC_<NAME>_*, PDM the username and password beside each [pypi.<name>] url. For an index this machine's own configuration names, the credential goes to that index's path — its URL without /simple (devpi's /+simple), so the JSON API, the files and their metadata below it are covered — over netrc's. For an index of that name only the repository defines, and for a $NAME or ${NAME} user name or password in a repository Pipfile or PDM source URL, the secret is lent as for Yarn and Bun: only to an index vouched for with --trust-index or on the host of one of this machine's PyPI indexes, Poetry repositories or explicit uv indexes. A password written into a repository index URL is discarded, and one Poetry keeps in the system keyring is not read.

In this machine's own Yarn Berry configuration an npmRegistries entry's credential goes to its registry, a scope's to the scope's npmRegistryServer, and the top-level one (or YARN_NPM_AUTH_TOKEN, YARN_NPM_AUTH_IDENT) to the default registry — the file's npmRegistryServer or YARN_NPM_REGISTRY_SERVER, else registry.yarnpkg.com and registry.npmjs.org. npmAuthToken is sent as a Bearer token and npmAuthIdent (user:password) as Basic credentials; a ${VAR} naming an unset variable without a fallback is dropped, as Yarn refuses it. What the npmrc holds for a registry wins over Yarn's and Bun's. A Yarn credential goes only with the requests Yarn sends it with: a scope's with the scope's packages, a registry's with every package under npmAlwaysAuth: true (or YARN_NPM_ALWAYS_AUTH) and otherwise with the scoped packages only, so without it an unscoped package is asked anonymously, as Yarn asks. YARN_NPM_SCOPES is not read: Yarn refuses a map from the environment.

Files above the analyzed directory. Yarn and NuGet also read their files (.yarnrc.yml, or the name YARN_RC_FILENAME gives it; nuget.config) in every directory above a project, and so does depphunter above the directory it analyzes. Up to the top of that directory's checkout (the nearest directory above it holding a .git) they are the repository's, like the files the scan finds: untrusted, and given no credential of this machine's. Above the checkout they are this machine's configuration — the ~/work/.yarnrc.yml that points every project at the company registry — trusted, with their credentials read. Yarn's are merged key by key, the closest winning, the home's .yarnrc.yml last.

An npm credential serves its registry's path. A key such as //gitlab.corp/api/v4/projects/1/packages/npm/:_authToken is sent only under that path, the longest matching key winning, so two projects' registries on one host each keep their own token and a Markdown link elsewhere on the host carries none; //host/:_authToken serves the whole host. Yarn's and Bun's credentials for a registry with a path are limited the same way.

Composer's home is the one Composer itself uses: COMPOSER_HOME when set, else $XDG_CONFIG_HOME/composer (~/.config/composer) or ~/.composer, whichever exists first (%APPDATA%\Composer on Windows). The same keys under the "config" of its config.json are read too, auth.json over them and COMPOSER_AUTH over both. An http-basic entry, and a gitlab-token with a username (a deploy or job token), is sent as Basic credentials; bearer, gitlab-oauth, github-oauth and a bare gitlab-token as a Bearer token, a github.com token to api.github.com as well. For a host named under several of these, the one Composer loads last wins, bearer over http-basic. bitbucket-oauth is not read: it is an OAuth consumer key and secret that Composer exchanges for a token on every run. The auth.json beside a repository's composer.json, and the "config" of a composer.json, belong to the repository and are never read.

Bundler names a credential after its server: bundle config set --global gems.example.com user:password writes BUNDLE_GEMS__EXAMPLE__COM, the host upper-cased with . as __ and - as ___, and a pipeline sets the same variable. The user's config is BUNDLE_USER_CONFIG, else config in BUNDLE_USER_HOME, else ~/.bundle/config; a variable replaces the file's entry. A key may also be a source URL (BUNDLE_HTTPS://RUBYGEMS__PKG__GITHUB__COM/ACME/), filed under its host; a host key wins over it. The value goes out as Basic credentials, both halves URL-unescaped as Bundler does; a bare token is the user name with an empty password. The application's .bundle/config, beside the Gemfile or wherever BUNDLE_APP_CONFIG points, belongs to the repository and is never read.

The JVM build tools share the Maven ecosystem. Maven's settings contribute the repositories of the profiles <activeProfiles> lists, or, when it lists none, of those with <activeByDefault>true</activeByDefault>; a profile activated by a property, the OS, the JDK or a file is not read, since that depends on the build, and <pluginRepositories> serve Maven's plugins. A <server> goes to the host of the mirror or repository with its id, in either settings file or in the Clojure CLI's user deps.edn, which tools.deps matches by repository name. ~/.sbt/repositories is asked beside Central unless -Dsbt.override.build.repos=true is in SBT_OPTS or JAVA_OPTS: then its list is all that is asked, in order (a project's .sbtopts is the repository's and is not read). COURSIER_REPOSITORIES replaces Coursier's defaults in the same way; ivy: and local entries are skipped. A replacement one tool configures applies to every Maven package. Gradle's PasswordCredentials convention is followed for the init scripts in its user home only: a repository a project's build script declares is not given a credential, since the repository would then choose where it is sent. Leiningen's credentials.clj.gpg is not decrypted, and Maven's settings-security.xml is not used to decrypt a {...} password.

Most container registries no longer store a credential in config.json; they name a helper instead, and depphunter runs it as docker login does — docker-credential-<name> get, with the registry on standard input. This is the only program depphunter executes that was not named on its command line, so the helper's name must be a bare name, is resolved on PATH only, and is given ten seconds; a configuration naming a path obtains nothing. A helper that returns an identity token rather than a password is not used, since only registries accept one.

Private and internal dependencies

Most of an enterprise repository is the organization's own code, and two of the operations depphunter performs for a public package must not be performed for such packages:

  • querying a public index for its dependencies, which does not answer and discloses to proxy.golang.org, or registry.npmjs.org, that the package exists; and
  • querying the OSV database about it, which discloses the name and version of internal code to a third party.

Neither case can be inferred — a module path on a company host is indistinguishable from any other — so the packages are declared:

depphunter --private 'corp.example/*' --private 'npm:@acme/*' .

A pattern is a glob with GOPRIVATE's semantics: it matches a package whose leading path elements match it, so that corp.example/* covers corp.example/team/billing. It applies equally to an npm scope (@acme/*), a Maven group (com.acme.*, which covers the artifact com.acme.billing:api; maven:com.acme:lib names one artifact) and a registry path (harbor.corp/*). Prefixing a pattern with an ecosystem — npm:, go:, maven:, nuget:, oci:, pypi:, crates:, actions:, gitlab-ci:, psgallery:, c-external:, vcpkg:, conan:, composer:, rubygems:, swiftpm:, pub:, hex:, cran:, bioconductor:, hackage:, terraform-module:, terraform-provider:, buf:, cmake-fetch:, pkg-config:, cocoapods:, carthage:, luarocks:, wally:, cpan:, opam:, julia:, zig:, bazel:, bazel-repo:, nix:, nixpkgs:, elm:, purescript:, shards:, paket:, dub:, fpm:, fortran-external:, haxelib:, alire:, raco:, quicklisp:, soldeer:, git-submodule:, nimble:, jsonnet-bundler:, cue:, dhall: or puppet-forge: — restricts it to that ecosystem.

GOPRIVATE, GONOPROXY and GONOSUMDB are read in addition to whatever is configured here — from the environment, or else from the file go env -w writes (GOENV, by default go/env in the user configuration directory), as the go command reads them — so a Go project whose machine is already configured requires no further setting.

A package matched in this way is drawn with a private label, is never named to that ecosystem's public index, and is never sent to the vulnerability database — neither its name and version nor, when it is a git dependency, its commit (a pattern matching its repository, github.com/acme/*, keeps the commit back too). It is still queried against an index this machine configures, since an internal registry already knows of it, so a private registry continues to answer for what its packages depend on — whether it replaces the public index or is asked beside it (an extra pip index, a NuGet feed). A private package that such an index does not have is not looked for on the public one, and one whose only other source is the repository's own index is drawn from that index, marked. private may also be set in a repository's own .depphunter.yaml; the only effect available to it is to make depphunter disclose less, and the repository is the authority on which of its dependencies are internal. A Python package installed from a directory, an archive or a version-control URL is treated as private without any pattern (see Languages).

Vouching for an internal index

An index that appears only in the repository is marked ⚠ index and never queried, because a repository directing a package manager at an index that nothing here configures is the form dependency confusion takes. In an organization whose repositories carry their own .npmrc naming the company registry, this marks every package, and a warning that is always present conveys nothing.

--trust-index identifies an index as the organization's own:

depphunter --trust-index https://nexus.corp/repository/npm-group .

It may be set only in the user's own configuration file, the environment or on the command line. A repository cannot vouch for itself; were that permitted, the marking would guard nothing.

Setting Flag Environment
private --private DEPPHUNTER_PRIVATE
trust_indexes --trust-index DEPPHUNTER_TRUST_INDEXES

Both are repeatable. A --private value may itself be a comma-separated list, and both environment variables take one; a --trust-index value is a single URL.

The resolution report

None of the preceding resolution is visible in the map it produces. A package resolving from an internal Nexus and one resolving from registry.npmjs.org are drawn identically, and a dependency tree that terminates two levels down is indistinguishable whether the dependencies end there, no lock file covers them, or a proxy returned 404. The resolution report records the difference.

depphunter --resolve-depth 2 --online --explain .

It is a single account of one analysis, available in three forms:

Form Intended use
--explain, written to the log a digest to read while the run is still in the terminal
GET /api/resolution the complete report as JSON, for comparison between runs or assertions in CI
?format=md, ?format=text the same report rendered; the Markdown form is what the editor opens

The report records six things:

  • the indexes known to the run — each index's URL, the scope it serves, and whether it was learned from this machine, from the repository, from the repository with --trust-index subsequently vouching for it, from a container image reference, or is the ecosystem's public default;
  • what resolved from where — one row per index, giving the number of packages resolving from it and how many of those are private, so that an unexpected index is a single row rather than a search through the map; packages installed from outside any index form a row of their own;
  • the walk — per ecosystem and per level: how many packages were queried, how many answered, how many were new, and the elapsed time; the answers line also counts the questions answered from what a Python environment has installed;
  • the ecosystems not walked at all — when --online is not given, an ecosystem whose dependency graph is held outside the repository (Go modules, NuGet, Maven, container images) is named, rather than silently contributing nothing;
  • what the resolvers noticed — notes a plugin or the index client adds once per file or registry, each with a code: a bun.lockb nothing reads (lock-unread); a lock that pins versions but records no edges, so that offline the walk adds nothing past it — rebar.lock without a mix.lock, a Conan 2 conan.lock, Package.resolved without .build/checkouts, pubspec.lock, an opam lock without dune.lock, luarocks.lock (lock-flat); a lock the scan left out as git-ignored that was read from disk, so it reflects this checkout's last install (lock-ignored); a Hex organization with no key on this machine (no-key) or whose API refused the key (forbidden, an organization key without api:read), or a GitHub API that refused to answer (forbidden, its limit without a token); and a NuGet package no packageSourceMapping pattern covers, which NuGet itself would not restore (unmapped); a CPAN release MetaCPAN does not describe (no-release); an opam repository, Alire index, Julia registry or CocoaPods spec repository only git serves, with no copy on this machine (no-copy); and a Bazel registry a .bazelrc names a credential helper for, or a Conan remote that wants a login only Conan's auth_remote.py plugin could supply, which is not run (helper-not-run); a Python package asked of several indexes while uv or PDM is configured to merge their versions (merged); a package-lock.json or yarn.lock git dependency locked to a branch or tag, not a commit (git-unpinned); a directory PYTHONPATH or a Python tool's settings add to the import roots (import-root); and
  • the questions nothing answered — every package for which no answer was obtained, with the reason: no lock file covers it; its index is named only by the repository; it is private and its index is the public one; the proxy requires a version it was not given; depphunter asks no index for this ecosystem, or cannot ask one because an import names no artifact; the package was installed from outside any index; or a request was made and returned a given status.

The last of these is the principal reason for the report. These reasons are indistinguishable on the map, each drawing a package with nothing beneath it, yet they mean entirely different things. A 404, or a 401 from a feed whose credentials are wrong, is a configuration fault that the map can express only as an absence.

the walk past what the code imports
  PLUGIN      LEVEL  ASKED  ANSWERED  ADDED  EDGES  TIME
  go          0      25     13        6      27     120ms
  javascript  0      7      3         9      10     3346ms

answers
  87 asked; 3 from lock files; 56 from indexes (56 fetched, 0 cached, 0
  already asked); 28 unanswered (4 of them asked and failed); 89 requests; 159
  external packages on the map (87 transitive, 0 private, 0 from an index
  nothing here vouches for)

what the resolvers noticed
  beam erl/rebar.lock (lock-flat): rebar.lock pins versions but records no
  edges, only a depth: offline, --resolve-depth adds nothing past the packages
  it pins (--online asks Hex)

nothing answered for these
  COUNT  WHY
  15     depphunter asks no index for this ecosystem
  8      this ecosystem's index cannot be asked (an import names no artifact)
  1      https://registry.npmjs.org/@scope%2ftool/1.2.0: 404 Not Found

The JSON retains what the written report abbreviates: every question, up to 20 000, ordered by level, ecosystem and package, and for each one the URLs requested, in the order made, with the status returned by each — which is how the three round trips a container image requires can be distinguished when one of them fails.

In the editor, depphunter: Show the Resolution Report opens the same report as a document beside the code, and the depphunter.explain setting writes the digest to the depphunter output channel whenever the map is built. Under --watch, the digest follows a re-analysis only where the map actually changed; a report after every saved file would obscure the one belonging to the change under examination.

CI pipelines

The code that executes with a repository's secrets is also a dependency, and it is declared nowhere a package manager reads. depphunter takes it from the pipeline files themselves — .github/workflows/*.yml and *.yaml, action.yml/action.yaml, .gitlab-ci.yml, *.gitlab-ci.yml and any YAML file under .gitlab/ — and places it on the map alongside the packages:

  • GitHub Actions: each step's uses:, reusable workflows (jobs.<id>.uses), and a composite action's own steps. A ./path resolves to the action.yml or workflow inside this repository, so a local action's own dependencies chain on. With --online, an action or reusable workflow of another repository is read at the reference the workflow names (see Package indexes): a composite action's steps, a Docker action's image and a reusable workflow's jobs are its dependencies. A repository used at several paths (actions/cache/save, actions/cache/restore) is one package, read at the first path met.
  • GitLab CI: every include: form - local, project (with ref and file), template, remote and component - plus the includes a bridge job triggers.
  • Container images: container:, services:, docker://… and a Docker action's runs.image on GitHub; image:, services: and default: on GitLab, per job and pipeline-wide.

Jobs become the symbols of their file, so a pipeline expands into its jobs the way a source file expands into its functions.

Pinning is stricter here than in a package ecosystem, because a reference that can be rewritten is not a pin: only a commit or a digest qualifies. actions/checkout@v4 floats, since the tag may be moved to other code, and so does nginx:1.25.3, since an image tag may be republished at its owner's discretion. The hardening convention of pinning to a commit and recording the version in a trailing comment is read as both: actions/setup-go@3041bf5… # v5.0.1 reports the commit as the version and v5.0.1 as what was requested. A GitLab template or a remote include names no version at all and may still change beneath the repository, so it is likewise treated as floating.

Container builds

A Dockerfile names the images an application is built on, and a Compose file names the images it runs beside; both are dependencies no package manifest records. depphunter reads Dockerfile and Containerfile under any casing, the variants named after them (Dockerfile.dev, api.Dockerfile), and the Compose files compose.yaml, compose.*.yaml and docker-compose*.yml, in either .yml or .yaml:

  • Dockerfile: the image of every FROM, COPY --from= and RUN --mount=…,from=, and the frontend a # syntax= directive names. A reference to an earlier stage, by name or index, is part of the build and not a dependency, nor is scratch. Named stages become the file's symbols.
  • Compose: a service's image:, unless the service has a build:, in which case image: is only the tag of the result and the service points at the Dockerfile inside the repository that builds it, so that file's own base images chain on. Images of an inline Dockerfile and docker-image:// build contexts are included; services become the file's symbols. A service that extends: another takes its image: and build: (a build: from another file keeps that file's directory), and the top-level include: (short and long form) and an extends: file: are edges to those files. An included file not named as a Compose file is read for the including one. Remote includes and files outside the repository are not followed.

Build arguments are expanded from their defaults, so ARG BASE=node:20 and FROM ${BASE} name node:20, and Compose's ${VAR:-default} likewise. Compose values also come from the .env file beside the Compose file (the included file's own for an included file), read from disk since it is often not committed. Its values only complete references: they are never shown, and a name that holds a credential (*PASSWORD*, *TOKEN*, *SECRET*, *_KEY and the like) is not read at all. A value only --build-arg or the environment supplies is not in the repository and is not guessed: an image whose name depends on one is shown as unresolved, under the reference as written, and one whose tag depends on one keeps the tag as written.

The images join those of the CI pipelines in one Container images island, with the same rule that only a digest pins, the same oci: private patterns and the same base-image lookup under --online. Docker Hub's long names (docker.io/library/nginx) are shortened to the name used everywhere else (nginx), so one image is one building however it is written.

Infrastructure as code

A Terraform or OpenTofu configuration installs modules and providers that execute with the credentials of the cloud it manages, and none of them appears in a package manifest. depphunter reads .tf and .tofu files (and their .tf.json form), .tfvars files, .terraform.lock.hcl and Terragrunt's .hcl files. A module is a directory, and its files are read together:

  • Modules: a module block's source. A local path (./modules/vpc) is an edge to that directory; a registry address (terraform-aws-modules/vpc/aws, with a host for a private registry and //subdir for a submodule) and anything fetched from git, a web server or a bucket (git::https://…?ref=v1.2.0, github.com/org/repo//sub) join the Terraform modules island, the latter named by the normalized URL.
  • Providers: required_providers entries, provider blocks and, where a module declares nothing, the provider a resource type implies (aws_instance uses hashicorp/aws), in the Terraform providers island. The lock file pins them for its module and the modules it calls. registry.terraform.io/ and registry.opentofu.org/ are dropped from names, since both registries serve the same namespaces.
  • Inside a module: var.x, local.x, module.x, data.t.n and aws_instance.web link the referring file to the file declaring them, and file() and templatefile() with a literal path link to the file read.
  • Terragrunt: the terraform block's source (with the locals of an included file substituted, as in Gruntwork's _envcommon layout), dependency and dependencies paths, and what find_in_parent_folders() finds.

Resources, data sources, modules, variables, outputs, locals and provider configurations become the file's symbols. Only a commit pins a git module; a registry module is pinned by an exact version, else by the version terraform init installed for the call (.terraform/modules/modules.json, read from disk), whose entries --resolve-depth also follows to the installed module's own calls. Nothing is evaluated, so a source or version computed from variables is not followed.

Jsonnet and CUE

Jsonnet and CUE generate the configuration that Kubernetes, Grafana and Prometheus then run, and their libraries come from git repositories and module registries that no other manifest lists.

Jsonnet libraries are installed by jsonnet-bundler (jb) into vendor/; the jsonnet-bundler packages island names each dependency by its repository and subdirectory (github.com/grafana/jsonnet-libs/ksonnet-util). depphunter reads .jsonnet and .libsonnet files, jsonnetfile.json and jsonnetfile.lock.json; what jb install wrote into vendor/ beside a jsonnetfile.json is not read as source:

  • Imports: import, importstr and importbin resolve as jsonnet finds the file: relative to the importing file, then in the vendor/ and lib/ directories of the jsonnet-bundler projects above it (nearest first), then JSONNET_PATH, then under the importer's ancestors (a tool run with -J at a parent directory). A file found in vendor/ belongs to the dependency that installed it, by its full path (github.com/org/repo/subdir/...) or its legacy link (vendor/<name>: the name field, else the subdirectory's last element); with nothing installed, jsonnetfile.json names the package the same way, and a local source leads to its files.
  • Manifests: every dependency of jsonnetfile.json and entry of jsonnetfile.lock.json is an import of its package. The lock pins (the declared version shown as requested); without it a commit pins, a tag is shown, neither pinned nor floating, and a branch floats. A nested project without a lock (kube-prometheus's library) is pinned by the lock that installs it. --resolve-depth follows the jsonnetfile.json of installed packages.

The file's leading locals and the fields of the object it returns are the symbols; imports in comments, strings and ||| text blocks are not read.

CUE packages are directories of a module (cue.mod/module.cue). The CUE modules island holds the dependencies module.cue declares (deps: "github.com/x/y@v0": v: "v0.3.1", named without the major version and pinned by v, since the modules system selects exact versions), the modules vendored the old way into cue.mod/pkg, and what cue get generated from other sources; CUE's builtin packages (strings, encoding/json, tool/exec) form a hidden CUE standard library. An import of the module's own path links to every file of that package in its directory. What cue get go generated into cue.mod/gen (and cue.mod/usr augments) is linked to the Go module go.mod requires for it — the same node the Go plugin draws — to the Go standard library, or to the module's own Go package directory. --resolve-depth follows a dependency's own module.cue in cue's module cache on this machine ($CUE_CACHE_DIR, else cue in the user's cache directory), at the versions the repository's module.cue selects. Files under cue.mod/pkg, cue.mod/gen and cue.mod/usr are not read as source. The package clause, top-level definitions (#Name) and fields are the symbols.

OSV has no Jsonnet or CUE ecosystem, so neither kind of package is asked about by name and version; a jsonnet-bundler package locked to a commit of a repository on a public forge is asked about by that commit. jsonnet-bundler has no registry; with --online a CUE module's own dependencies are read from its registry (registry.cue.works, or the one CUE_REGISTRY routes it to).

Dhall, Puppet and Rego

Three more configuration languages: Dhall programs import files and URLs, Puppet manifests name classes of modules from the Forge, and Rego policies import each other's packages.

Dhall (.dhall): every import is an edge — a relative path to its file (as Text, as Location and as Bytes too, and every branch of an alternative a ? b), a URL to a package of the Dhall packages island. The Prelude is github.com/dhall-lang/dhall-lang/Prelude, whether it comes from prelude.dhall-lang.org or dhall-lang's repository; a file GitHub, GitLab or jsDelivr serve raw belongs to its repository, with the reference as its version; any other URL is named by its host and its path up to a version segment (example.com/dhall for .../dhall/v1.2.0/util.dhall) or its directory. A sha256: hash pins the import, since Dhall refuses content that does not match it; without one a URL naming no version or a branch floats. Absolute and home-relative paths and env: imports name nothing in the repository and are dropped. The bindings of a file's leading let chain and the fields of the record it returns are its symbols. spago's spago.dhall, packages.dhall and test.dhall stay with the PureScript plugin, which reads them with the same Dhall reader.

Puppet (.pp, Puppetfile, a module's metadata.json and .fixtures.yml): include, require, contain, class { 'x': }, inherits, Class['x'], declarations, references and collectors of defined and custom types, namespaced function calls, namespaced data types (Stdlib::Port), template(), epp(), file() and puppet:///modules/ sources name a module by their first segment.

  • A module of the repository (site-modules/, site/, dist/, or the repository itself, named by its metadata.json) links to the file Puppet's autoloader reads: profile::web to site-modules/profile/manifests/web.pp, a function to functions/ or lib/puppet/functions/, a type alias to types/, a template to templates/.
  • Another module is what the module's own metadata.json and .fixtures.yml, else the Puppetfiles above the file, declare: a Forge module by its slug (puppetlabs-stdlib), a git module by its repository. A module r10k installed into modules/ beside the Puppetfile is named by its installed metadata.json, and --resolve-depth follows that file's dependencies; modules/ and spec/fixtures/modules are not read as source.
  • Puppet's own resource types (file, package, service, exec, ...), data types and functions are no dependency; stdlib's unnamespaced functions (merge(), pick()) and well-known types (file_line) are stdlib's. Comments, strings, heredocs and regular expressions are not read, and a Free Pascal .pp unit yields nothing.

Classes, defined types, nodes, functions, type aliases and Bolt plans are the symbols.

Rego (.rego): import data.a.b links to every file declaring package a.b — or, for data.a.b.rule, the longest package the path starts with — and so do references to data.a.b... in rules, directly or through an imported name, unless an import already links that package. import input, import rego.v1 and import future.keywords are built in; data no policy declares (JSON documents, bundles) is dropped, since OPA has no package manager. The package, rules and functions are the symbols.

OSV has no Dhall or Puppet Forge ecosystem and Dhall has no registry; only a Dhall import URL or a git-fetched Puppet module naming a full commit of a repository on a public forge is asked about, by that commit. With --online the Forge (or the one a Puppetfile's forge line or r10k's forge.baseurl names) is asked for a module's dependencies.

Shaders and GPU code

GPU code comes in two families. CUDA, OpenCL C and Metal are dialects of C and C++, and the C/C++ plugin reads them; GLSL, HLSL and WGSL are shading languages of their own, which a shader plugin reads.

CUDA (.cu, .cuh) and Metal (.metal) are read as C++, OpenCL C kernels (.clh, and .cl files whose head shows a directive or a kernel; any other .cl file is Common Lisp's) as C, with the includes and include path of C and C++: a .cu file links to its .cuh and .h headers, a Metal shader to the header it shares with the app. Kernels (__global__, __kernel, kernel, vertex, fragment) and device functions are function symbols; __launch_bounds__(N) is stepped over and a launch (k<<<grid, block>>>()) is a call. The toolkits' headers are standard-library islands, tried after the project's own files (a repository vendoring Thrust or CUB links to its copy): the CUDA Toolkit (cuda_runtime.h, cuda_fp16.h, the cuBLAS, cuFFT, cuRAND, cuDNN, NPP and NVTX headers, each as written; thrust/, cub/, cuda/ (libcu++), nv/, nvtx3/, cooperative_groups/ and crt/ by directory), the OpenCL headers (CL/cl.h, CL/opencl.hpp; not SYCL's CL/sycl.hpp), and the Apple SDKs, where <metal_stdlib> is Metal's, <simd/simd.h> simd's and <OpenCL/opencl.h> the OpenCL framework's — the same nodes a Swift import Metal reaches.

GLSL (.glsl, .vert, .frag, .geom, .tesc, .tese, .comp, the ray tracing stages .rgen, .rchit, .rahit, .rmiss, .rint, .rcall, .vsh, .fsh, and .fs, .vs, .gs, .mesh and .task when their head shows GLSL) and HLSL (.hlsl, .hlsli, .fx, .fxh, and Unreal's .usf and .ush): #include is read by the C preprocessor's scanner (comments, #if 0) and resolved as a C header of the project is - beside the includer, a compilation database's include paths, include/, src/, a unique file ending in the path — then against the includer's parent directories, then ignoring case (HLSL is mostly written on Windows). Unreal's virtual paths link to the engine's or the plugin's Shaders/ directory when the repository has it (/Plugin/MyFX/x to Plugins/MyFX/Shaders/x), else to the Unreal Engine shaders island (Engine, or the plugin's name); a GL_ARB named string (/x.glsl) to the file from the root. Whatever else a shader includes is handed to the compiler by the application and dropped. Functions (entry points with their semantics and attributes), structs, GLSL interface blocks (uniform Camera { ... }), cbuffers, techniques and HLSL namespaces are the symbols.

WGSL (.wgsl, and WESL's .wesl) has no include; Bevy's naga_oil adds #import a::b::item (with {...} lists over several lines, as and quoted files) and #define_import_path a::b, WESL import package::a::item;, super:: and package names. A module path links to the file declaring it with #define_import_path, to the file its path names below the package root (package::render::view to src/render/view.wesl) or the importer's directory (super::), and a quoted file to the file beside the importer or below assets/ (Bevy's asset path). A path whose first segment is a crate the Cargo manifests declare links to that crate: a crate of the workspace to the module's file below its src/, a crates.io dependency to its crates.io package — and Bevy's crates (bevy_pbr, bevy_render, ...), which ship the shaders a game imports, to themselves when the game declares bevy, pinned by Cargo.lock or floating on bevy's requirement. fn, struct, var, const, override and alias declarations are the symbols.

Preprocessor and naga_oil conditions other than #if 0 are not evaluated (every branch counts), and engine virtual paths other than Unreal's are not known. glsl_analyzer, shader-language-server and wgsl-analyzer serve --lsp; a sniffed .fs shader still goes to the F# server and an OpenCL .cl kernel to the Common Lisp one (servers are chosen by extension).

Nix

A Nix project states how it is built, what it builds with and which revision of nixpkgs everything comes from. depphunter reads .nix files, flakes (flake.nix and flake.lock) and the pins of niv (nix/sources.json) and npins (npins/sources.json), without evaluating anything:

  • Files: import ./x.nix, callPackage ./x { } and a NixOS module's imports = [ ./a.nix ./b ] are edges to the file, a directory meaning its default.nix; any other relative path (builtins.readFile ./VERSION, src = ./src, "${./script.sh}") is an edge to that file or directory. Text inside strings is not read, interpolations are.
  • Flake inputs: each input of flake.nix is a package of the Nix flakes and sources island named by its URL: github:NixOS/nixpkgs/nixos-24.05 is github.com/nixos/nixpkgs (GitHub names in lower case), gitlab:, sourcehut:, git+https://… and archives by their repository, FlakeHub by flakehub.com/f/owner/repo. flake.lock pins them at the locked commit (shown shortened, ad57eef), with the branch or tag asked for as the requested version; its nodes' own inputs, follows resolved, are what --resolve-depth follows. A path: input is an edge to that flake, a follows the followed input, and inputs.x in a module the flake's input.
  • Registry names and channels: <nixpkgs>, flake:nixpkgs and an outputs argument no input declares name the registry alias (nixpkgs) rather than what each machine's NIX_PATH or flake registry makes of it, so they float. A NixOS channel's tarball is nixpkgs at that channel.
  • niv and npins: each source is a package pinned by its revision or hash, and sources.nixpkgs (or pins.nixpkgs) in a file that imported the loader is an edge to it. builtins.fetchTarball, fetchGit, fetchTree and getFlake with literal arguments are named the same way.
  • Nixpkgs packages: the attributes in buildInputs, nativeBuildInputs, propagatedBuildInputs, checkInputs, packages (mkShell, home.packages) and environment.systemPackages - pkgs.jq, with pkgs; [ openssl zlib ], the arguments of a callPackage-style file - are packages of the Nixpkgs island, versioned and pinned by the project's nixpkgs input (or niv's or npins' nixpkgs). Inside nixpkgs itself they are its pkgs/by-name files.

Top-level let bindings and the attributes a file returns (paths cut at two names) are the symbols, and a flake's outputs (packages.default, nixosModules.default). The pinning rule is the one used elsewhere: a lock, a commit or a content hash pins, a tag neither pins nor floats, a branch or nothing floats. There is no Nix package index for --online to ask, and no vulnerability database covers Nix by name; a flake input, niv or npins source locked to a git commit on a public forge is asked about by that commit.

Gleam

Gleam compiles to Erlang and JavaScript and publishes to Hex, so its packages are Hex packages: they share the Hex island, its pinning rule, OSV's Hex advisories and the hex.pm client of --online with Elixir and Erlang, and a package that a Gleam module imports and a mix.lock locks is one building. depphunter reads .gleam modules, gleam.toml and the manifest.toml Gleam writes beside it, without running gleam:

  • Modules: import a/b/c (with or without .{type T, f} and as c) is an edge to src/a/b/c.gleam, test/… or dev/… of the importing package, or of a path dependency; a module of a package gleam downloaded into build/packages/ names that package when the directory is on disk.
  • Packages: other modules belong to the declared or locked package named by their leading segments joined by _: lustre/element is lustre, gleam/erlang/process gleam_erlang, gleam/otp/actor gleam_otp. gleam/list, gleam/string and the rest of the standard library are gleam_stdlib, a versioned package like any other, not a hidden island.
  • Externals: @external(erlang, "mod", "f") is an edge to the project's mod.erl, an Erlang/OTP module, the package of that name or application (hpack is hpack_erl), or the compiled Gleam module gleam@list names; @external(javascript, "./ffi.mjs", "f") to that file, or to the package a path climbing out of the package names; a bare specifier is an npm package.
  • Manifests: gleam.toml's dependencies and manifest.toml's packages are imports of what they name. The manifest pins, keeps a range asked for as the requested version, and gives --resolve-depth each package's requirements; without it == 1.2.3 or a bare 1.2.3 pins, a git commit pins and a branch floats. A path dependency is an edge to its gleam.toml.

Functions, constants, types and their constructors (Order.Cancelled) are the symbols. Elixir and Erlang code calling a compiled Gleam module (:gleam@list.map, gleam@list:map) reaches the same file or package.

Elm

Elm applications record the exact version of every package they install, and packages declare ranges; the Elm packages island names them author/name, as elm.json and package.elm-lang.org do. depphunter reads .elm modules and elm.json, without running elm; elm-stuff/ is not read:

  • Modules: import A.B (with or without as and exposing) is an edge to A/B.elm under the source directories of the project the file belongs to: an application's source-directories, a package's src/, and tests/ for elm-test. An examples application listing ../src reaches the library's files; when several elm.json files list a file, the one whose own directory holds it comes first.
  • Packages: a module of a package the compiler installed in ELM_HOME (else ~/.elm) is that package, by its exposed-modules. Without it, elm/core's modules (Dict, List, Task, ...) are elm/core, a versioned package like any other, and other modules go to the listed package a curated table (Html elm/html, Html.Styled rtfeldman/elm-css, Json.Decode.Pipeline NoRedInk/elm-json-decode-pipeline) or the package's own name (List.Extra elm-community/list-extra) names. A module nothing names is dropped: its name does not say which author published it. The modules Elm imports by default are not edges.
  • Manifests: every package elm.json lists — direct, indirect and test dependencies alike — is an import of it, and each source directory an edge to that directory. An application's exact versions pin; a package's ranges (1.0.0 <= v < 2.0.0) float. --resolve-depth follows the elm.json of packages installed in ELM_HOME, and --online asks package.elm-lang.org. For a package, the compiler elm-tooling.json pins decides which ELM_HOME directory is looked in first; the other tools it pins (elm-format, elm-json, elm-test-rs) are programs, not dependencies.

Functions and values, types and type aliases, the constructors of custom types (Msg.Clicked), ports and infix operators are the symbols. OSV has no Elm ecosystem, so Elm packages are not checked for advisories.

PureScript

PureScript projects are built by spago from a package set or the registry's ranges; the PureScript packages island names packages as the registry does (prelude, halogen), and packages installed from git by their repository. depphunter reads .purs modules, spago.yaml, spago.lock, spago 0.20's spago.dhall and packages.dhall, and a legacy bower.json, without running spago; .spago/ is not read as source, and neither is output/ beside a spago configuration:

  • Modules: import A.B (qualified, with an import list or hiding) is an edge to the file whose module header names A.B, among the sources of the importing file's package first — src/ and test/ of a spago.yaml package, the sources globs of a spago.dhall — then its workspace's other packages. A module with a foreign import needs its JavaScript companion, an edge to the .js file of the same name beside it. The Prim modules (Prim, Prim.Row, Prim.TypeError, ...) come with the compiler: a hidden PureScript built-ins island.
  • Packages: a module of a package spago installed in .spago/ (or bower in bower_components/) is that package. Without it, a curated table (prelude's modules, Data.Map ordered-collections, Effect.Aff aff, Control.Monad.State transformers, Halogen.* halogen) or the listed package the module spells (Node.FS node-fs, Data.Maybe maybe) names it. A module nothing names is dropped.
  • Manifests: every package a manifest lists or spago.lock records is an import of it. spago.lock pins (with spago.yaml's range as the requested version); a registry range floats; a bare name follows the package set, whose name (registry 60.0.0, psc-0.15.0-20220507) is shown as the version, neither pinned nor floating, since the set's versions are not known offline. Git packages (extraPackages, a packages.dhall override) are named by their repository and pinned by a commit; a tag is shown, neither. The Dhall reader follows let, //, #, with and local imports, and names the remote package set without fetching it. --resolve-depth follows spago.lock, else the manifests (spago.yaml, spago.dhall, purs.json) of the packages spago installed into .spago/, and --online asks the registry's index.

Values and functions, data and newtype types with their constructors, type synonyms, classes with their members, named instances, foreign imports and infix operators are the symbols. OSV has no PureScript ecosystem, so PureScript packages are checked for advisories only by the commit a git package is pinned to.

Crystal

Crystal applications and libraries get their shards from git repositories through shards; the Crystal shards island names each shard as shard.yml does (kemal, db), the name require and lib/ use. depphunter reads .cr files, shard.yml, shard.lock and shard.override.yml without running the compiler or shards; lib/ beside a shard.yml, where shards installs, is not read as source (a lib/ directory elsewhere is), and neither is the compiler's .crystal/ cache:

  • Requires: require "./x" and require "../x" are edges to x.cr (or x/x.cr) relative to the file; require "./dir/*" to each .cr file of dir and require "./dir/**" to each below it. A require by name is looked up the way the compiler's CRYSTAL_PATH does: a shard installed in lib/ (the directory is the shard), then the project's own files (a shard requiring itself by name, as its specs and bin/ templates do; Crystal's standard library requiring itself from src/), then the directories of the repository CRYSTAL_PATH lists (relative to the repository's root, or absolute inside it), by the same rules as lib/, then a shard the manifests name, then the standard library by its first segment (json, http/client, digest/sha256: a hidden Crystal standard library island), then a declared shard spelled with -/_ or a crystal- prefix (sqlite3 is crystal-sqlite3). Other names are unresolved shards named by their first segment.
  • Manifests: every dependency and development dependency of a shard.yml (github:, gitlab:, bitbucket:, codeberg:, git:, path:) is an import of it, and every target's main: file an edge; a shard.override.yml replaces the entries it names. shard.lock pins (1.2.3, 0.3.1+git.commit.<sha>), with the requirement as the requested version; without it a commit: pins, a tag: or an exact version: is shown, neither pinned nor floating (a tag can be moved), and a branch:, a range and no requirement float. A shard from a git server other than GitHub, GitLab, Bitbucket, Codeberg or sourcehut carries its URL as its origin, and a path dependency is an edge to its directory. --resolve-depth follows the shard.yml of shards installed in lib/, and lib/.shards.info pins what shard.lock does not name; there is no index for --online.

Modules, classes, structs, enums, libs (C bindings with their funs, structs and unions), annotations, methods (Owner.name, as in Ruby), macros, constants, aliases, records and the attributes of getter and property are the symbols; what a macro defines is not, and @[Link("ssl")] libraries are not mapped. OSV has no Crystal ecosystem, so shards are checked for advisories only by the commit shard.lock pins them to.

F# and Paket

F# projects get their packages from NuGet, through <PackageReference> items or through Paket. depphunter reads .fs, .fsi and .fsx files, .fsproj files and Paket's paket.dependencies, paket.lock and paket.references without running the compiler, MSBuild or Paket. F# and C# share one NuGet reader, so a package both languages use is one building of the NuGet island; FSharp.Core is a NuGet package like any other (the SDK references it implicitly), not part of the hidden .NET base library. A .fs file that is a GLSL fragment shader or Forth is told apart by its first lines and not read as F#; what Paket installed (packages/ and paket-files/ beside paket.dependencies) and FAKE's .fake/ cache are not read either:

  • Compile order: a project compiles its files in the order its .fsproj lists them (<Compile Include> items are edges of the project file), and a file can only use what earlier files declare. open X.Y, open type X.Y, a module abbreviation (module P = Shop.Pricing) and a qualified name in code (Cart.add, Shop.Domain.Cart.empty) are edges to the files declaring that namespace, module or type - as written or relative to the enclosing namespaces and what is opened, an [<AutoOpen>] module's contents also under its parent - among the earlier files of the project and the files of the projects it references (<ProjectReference>, C# projects too). A name only later files declare is dropped.
  • Packages: an open no project file declares goes to a script's #r "nuget: ..." package, a declared package whose id prefixes the namespace (or extends it: Fake.Core is Fake.Core.Target), FSharp.Core (Microsoft.FSharp.*, FSharp.Collections, FSharp.Control, ...), the .NET base library (System.*, Microsoft.*) or an unresolved package. <PackageReference> items, Directory.Packages.props versions and the packages of paket.references are imports, and so is every line of paket.dependencies and every entry of paket.lock.
  • Paket: paket.lock pins (the paket.dependencies constraint is the requested version) and records the graph between packages, which --resolve-depth follows offline; without it Paket's = 1.2.3 and bare versions pin and ~> and >= float. GitHub, gist, git and HTTP dependencies form a Paket git, GitHub and HTTP sources island named by where they come from (github.com/fsharp/FAKE), pinned by the locked commit. Each directory with a paket.dependencies is a root of its own.
  • Scripts: #load is an edge to the script (relative to the file or an #I directory), #r "nuget: X, 1.2.3" a package (pinned when exact), #r of an assembly Paket installed under packages/ that package, of a framework assembly (System.Xml.Linq) the base library; other assemblies are dropped.

Namespaces, modules (with their nested modules), let bindings, types with their members, exceptions and the vals of signature files are the symbols. Unqualified names an open brings in are not linked (that needs the compiler), MSBuild conditions are ignored, and an F# open of a C# project's namespace is not linked to it.

D and dub

D applications and libraries get their packages from the dub registry through dub; the dub packages island names each package as the registry publishes it (vibe-d, mir-algorithm), a sub-package (vibe-d:http) by its base package. depphunter reads .d and .di files, dub.json, dub.sdl and dub.selections.json without running the compiler or dub. A .d file that is a make dependency file (app.o: app.d ..., as gcc -MD and dmd -makedeps write) or a DTrace script is told apart by its first lines and not read as D, and dub's .dub/ directory is not read:

  • Imports: import a.b.c; (lists, renamed, selective, static and public imports, and imports inside functions) is an edge to a/b/c.d or a/b/c/package.d under the import directories of the file's dub package (importPaths and sourcePaths, else source/ or src/) and of the repository's packages it depends on (sub-packages, path dependencies), else under the file's own directory and its ancestors (so Phobos' and druntime's own modules link in their repositories). import("file") (and mixin(import("file"))) is an edge to the file under stringImportPaths (views/ by default). Nothing in comments, strings, token strings q{ } or after __EOF__ is read.
  • Packages: druntime's and Phobos' modules (core.*, std.*, etc.c.*, object) are a hidden D runtime and standard library island. Another module goes to the package dub fetched onto this machine that has it (.dub/packages, $DUB_HOME, $DPATH or ~/.dub/packages), else to the declared package its leading segments spell (mir.random is mir-random, unit_threaded unit-threaded) or a curated table names (vibe.* is vibe-d, or vibe-core, vibe-http, ... when declared, arsd.dom arsd-official), else to an unresolved package named by the table or the module's first segment.
  • Recipes: every dependency of dub.json or dub.sdl - its configurations' and inline sub-packages' too - is an import of it, a sub-package directory an edge to it, and a single-file package's /+ dub.sdl: +/ recipe an import of its module. dub.selections.json pins (with the recipe's specification as the requested version; a repository entry by its commit, with the repository as origin); without it ==1.2.3 and a bare 1.2.3, which dub reads as exact, pin, and ~>, ^, >=, * and ~branch float. A path dependency is an edge to its directory. --resolve-depth follows the recipes of packages dub fetched; --online asks the registry (code.dlang.org).

Modules, classes, structs, interfaces, unions, enums, templates and mixin templates (nested ones qualified), functions and members (Owner.name, Owner.this), aliases and top-level manifest constants are the symbols. What a mixin generates is not read, every version and static if branch counts, and OSV has no D ecosystem, so dub packages are checked for advisories only by the commit a git dependency is pinned to.

Fortran and fpm

Most Fortran is built by CMake or Make, and newer libraries by the Fortran Package Manager (fpm), which fetches git and registry packages; the fpm packages island names each by its dependency key in fpm.toml (json-fortran, toml-f, stdlib). depphunter reads free-form (.f90, .F90, .f03, .f08, ...) and fixed-form (.f, .for, .f77, .F, ...) sources, fypp templates (.fypp) and fpm.toml without running the compiler, the C preprocessor, fypp or fpm. A .f or .for file that is Forth is told apart by its first lines, and fpm's build/ directory is not read as source:

  • Modules: use m (use, intrinsic ::, only: lists and renames) is an edge to the file that defines module m, found by name (case-insensitive) among all the project's sources, whatever the build system; a submodule is an edge to its parent module's or parent submodule's file. Intrinsic modules (iso_fortran_env, iso_c_binding, ieee_*, omp_lib as openmp, openacc) are a hidden Fortran intrinsic modules island; mpi, mpi_f08, hdf5, netcdf, PETSc, FFTW and MKL modules are the C libraries #include <mpi.h> names too, on the C/C++ external island. Another module goes to the dependency fpm fetched into build/dependencies/ that defines it, else to the declared dependency its name spells (json_module is json-fortran, tomlf toml-f, stdlib_kinds stdlib), a module [build] external-modules lists (the Fortran external modules island), a curated table's package, else an unresolved module of that island. Nothing in comments or strings is read, !$ OpenMP lines are, and every #ifdef branch counts.
  • Includes: include 'file', #include "file" and fypp's #:include are edges to the file beside the includer, else in fpm's include-dir or an ancestor's include/; mpif.h and fftw3.f03 go to their C library.
  • fpm.toml: every dependency ([dependencies], [dev-dependencies], features' and programs' own) is an import of it. A git rev pins, a tag is shown, neither pinned nor floating, and a branch or no ref floats; a registry dependency's v pins; a metapackage (stdlib = "*") floats, openmp is the intrinsic modules and mpi, hdf5, netcdf and blas the C libraries. A git server other than the public forges is the package's origin. fpm keeps no lock file: what build/cache.toml records it fetched is shown as the version, and --resolve-depth follows the fpm.toml of what it fetched into build/dependencies/. path dependencies, [library] source-dir and programs' main files are edges.

Modules, submodules, programs, block data, derived types, generic interfaces and functions and subroutines (Module.proc, with any prefix: pure elemental real(dp) function) are the symbols; interface bodies are not. fypp templates are not expanded (names such as ${k}$ are skipped), old-style external procedures and call statements are not linked, and OSV has no Fortran ecosystem and fpm's registry no dependency API, so fpm packages are not asked about by --online and are checked for advisories only by the commit a git dependency is pinned to.

Haxe and haxelib

Haxe compiles to JavaScript, C++, the JVM, Python, Lua, PHP, HashLink and more; its libraries come from haxelib, pinned by lix in haxe_libraries/<name>.hxml, and OpenFL and Lime games describe their build in Project.xml. The haxelib libraries island names each by its haxelib name (openfl, tink_core, thx.core). depphunter reads .hx modules, .hxml build files, haxelib.json, lix's pins and Lime project files without running the compiler, haxelib or lix; a project.xml is read only when it is a Lime <project> naming a haxelib or a source path, and a local haxelib repository (.haxelib/) is not read as source:

  • Modules: import a.b.C (with as/in aliases, sub-types a.b.C.D, fields a.b.C.f, a.b.C.* and std. paths), using a.b.Tools and qualified type names in code (haxe.Json.parse(...)) are edges to the module's file, found by the package every module of the repository declares, whatever the class paths; import a.b.* is an edge to every module of the package. The standard library (haxe, sys, js, flash, cpp, hl, the other targets' packages and top-level types such as StringTools and Lambda) is a hidden Haxe standard library island. Another module goes to the library haxelib or lix installed that has it, else to the declared library its package spells (tink.core is tink_core), a curated table's library (hxd is heaps, js.node hxnodejs, haxe.ui haxeui-core), else to an unresolved library; nothing in comments, strings, interpolations or regular expressions is read, and every #if branch counts. Each module has an edge to the import.hx files the compiler applies to it: in its directory and those above, up to its class path.
  • Build files: .hxml files' -lib (-L, --library), -cp, -main, root modules, -resource, included .hxml files and the classes --macro calls name, over all --next sections; haxelib.json's dependencies and classPath; a Lime project's <haxelib>, <source>, <classpath>, <app main> and <include>. The libraries a file may use are those of the build files in its directory and above.
  • Pinning: a lix pin fixes a haxelib version or a git commit (a tag is shown, a branch floats); without lix, -lib x:1.2.3, an exact version in haxelib.json or <haxelib version> and a git: commit pin, a git tag is shown, and no version floats, showing the version installed. A library in development inside the repository (lix's ${SCOPE_DIR}, or the repository's own haxelib.json) is an edge to its class path.
  • Installed libraries: haxelib's local .haxelib/ and global repository (HAXELIB_PATH, ~/.haxelib, ~/haxelib) and lix's cache (HAXE_LIBCACHE) say which library has a module, and --resolve-depth follows the -lib lines of lix pins and installed libraries' haxelib.json.

Types (class, interface, enum, enum abstract, abstract, typedef), their functions (Type.name) and module-level functions and variables are the symbols. Macros are not run, same-package types used without an import are not linked, and OSV has no Haxe ecosystem and lib.haxe.org no JSON API, so haxelib libraries are not asked about by --online and are checked for advisories only by the commit a git library is pinned to.

Ada, GPR and Alire

Ada (SPARK included) keeps a unit's spec in a .ads file and its body in an .adb file, GPRbuild builds from GNAT project files (.gpr), and Alire fetches crates named in alire.toml. The Alire crates island names each crate by its crate name (aunit, gnatcoll, utilada). depphunter reads sources, project files, alire.toml and Alire's lock file without running GNAT, GPRbuild or alr; Alire's alire/ directory beside an alire.toml and a build's obj/ beside a project file are not read as source:

  • Units: with A.B, C; (limited, private and limited private too, compared without case), a body's own spec, a child unit's parent and a subunit's (separate (P)) parent body are edges to the file that declares the unit, found by the unit every source of the repository declares, so GNAT's file naming, krunched names and a Naming package need not be followed; files in the source directories of the importing file's projects win. Predefined units (Ada.*, System.*, Interfaces.*, GNAT.*, Standard and the Ada 83 renamings such as Text_IO) are a hidden Ada predefined units island. Another unit goes to the crate Alire fetched that has it or its parent unit, else to the declared crate its name spells (Acme_Log.Sinks is acme_log, TOML ada_toml) or a curated table's crate (AWS, GNATCOLL, AUnit, XML/Ada's DOM and Sax, GtkAda's Gtk, Libadalang, VSS), else to an unresolved crate named by its first segment; a unit Alire generates (<crate>_config) is dropped. Nothing in comments, strings or character literals is read, attribute ticks (X'First) are told from character literals, and only the first branch of a gnatprep #if counts.
  • Project files: with "x.gpr", extends and an aggregate's Project_Files are edges to the repository's project file, else to the crate that ships it; Source_Dirs (src/** too) and Main are edges to the directories and main files. A project file is not evaluated: every case alternative counts, and an external gives its default.
  • Manifests: alire.toml's [[depends-on]] (every case(os) alternative), [[pins]] and project-files are imports. The lock file (alire/alire.lock, or alire.lock beside the manifest before Alire 1.1) pins what it solved; otherwise =1.2.3 and a bare 1.2.3 pin and ^, ~, >= and * float, showing the version Alire fetched. A pin to a git commit pins, a branch or no commit floats, a path pin in the repository is an edge to its alire.toml, and a git server other than the public forges is the crate's origin.
  • Fetched crates: the crates in alire/cache/dependencies/ and alire/cache/pins/, and those the manifest names in Alire 2's shared cache (~/.local/share/alire/releases), say which crate has a unit or ships a project file; --resolve-depth follows the lock file's solution, else a fetched crate's alire.toml, and --online reads the release manifest of an exact version, or of the newest release a range admits, from the indexes alr uses (see Package indexes).

Library units, the packages, subprograms (Unit.Name) and types (class for tagged types, struct for records, interface, enum, task, protected) of packages, and subunits are the symbols. Scenario variables are not evaluated, units generated at build time are unresolved, and OSV has no Ada ecosystem, so Alire crates are checked for advisories only by the commit a git pin names.

Racket and raco

Racket modules (.rkt, .rktl load files, .scrbl Scribble documents) start with a #lang line and require other modules by a relative path ("util.rkt") or a collection path (racket/list: the file list.rkt of the collection racket); raco installs the catalog packages that provide collections, named in a package's info.rkt. The Racket packages island names each package as raco does (rackunit-lib, gui-lib, a git source by its repository or ?path= name). depphunter reads modules and info.rkt without running Racket or raco; .rktd data files are claimed with nothing to link, a .scm or .ss file only when a #lang line starts it (else it is Chez Scheme, Guile or R6RS), and nothing in a compiled/ directory:

  • Modules: the #lang line (each meta-language such as at-exp, and the language's lang/reader.rkt or module), #reader, and at module level (submodules and begin included) require in every form (only-in, except-in, prefix-in, rename-in, for-syntax, for-label, for-meta, submod, lib, file, planet, multi-in, path-up), Typed Racket's require/typed family, lazy-require, include, load, and Scribble's @(require ...) and include-section. Comments (;, nested #| |#, #; datum comments, @;), strings, here strings, regexps, characters, quoted data and Scribble text are not read.
  • Resolution: a relative path is an edge to the file (a .ss path to the .rkt beside it); a collection path to the repository's collection when one has the file: the directory of a package's info.rkt (its collection, else the package's name), every directory of a multi-collection package ((define collection 'multi)) and of a collects/ tree, so monorepos such as racket/racket and typed-racket link across packages. Else the base collections (racket/*, syntax/*, net/url, json, data/queue, ...) are a hidden Racket base collections island; other collections go to the package a curated table of the main distribution names (rackunit is rackunit-lib, typed/racket typed-racket-lib, racket/gui gui-lib, net/smtp net-lib) or the declared package the collection spells (rebellion, acme-log for acme-log-lib), else to an unresolved package named by the collection. The declared packages are those of the file's own package.
  • Packages: info.rkt's deps and build-deps are imports (a package of the repository is an edge to its info.rkt). raco keeps no lock file: a git source's #<commit> and a #:checksum pin, a version tag is shown, and a branch, a #:version (a minimum) and no version float. A git server other than the public forges is the package's origin; a PLaneT module path is a planet/owner/pkg package.

Module-level definitions are the symbols (define as func or var, class values with their define/public methods, struct, macros, Typed Racket's define-type, and module submodules with their definitions as sub.name). Macros are not expanded and installed packages are not read; with --online the package catalog (pkgs.racket-lang.org) is asked for a package's dependencies. OSV has no Racket ecosystem, so Racket packages are not checked for advisories.

Common Lisp and Quicklisp

Common Lisp sources (.lisp, .lsp, and .cl unless an OpenCL kernel) are built by ASDF from the systems .asd files define, and their dependencies come from Quicklisp dists, pinned by Qlot (qlfile, qlfile.lock) or ocicl (ocicl.csv). The Quicklisp projects island names each dependency by the Quicklisp project releasing it (cl-ppcre for cl-ppcre-unicode, cl-str for str, a git source by its repository). depphunter reads sources and .asd files without running a Lisp; what Qlot installed into .qlot/ and ocicl into systems/ beside an ocicl.csv is not read as source, only its .asd files for --resolve-depth:

  • Systems: a defsystem's :components are edges from the .asd to its files (modules, :pathnames and component types followed), and its :depends-on (with (:version ...), (:feature ...), (:require ...)) name systems: one an .asd of the repository defines (secondary foo/bar systems too; a package-inferred system's foo/bar/baz is the file bar/baz.lisp) is an edge to it, ASDF, UIOP and SBCL's contribs go to the hidden Common Lisp built-ins island, others to their Quicklisp project. ql:quickload, asdf:load-system, require and load in scripts name systems and files likewise.
  • Packages: a defpackage's (or uiop:define-package's) :use, :import-from, :local-nicknames and :mix, in-package and each qualified symbol's package (ppcre:scan, once per file) go to the file of the repository defining the package; the standard's, an implementation's (sb-ext), ASDF's and UIOP's to the built-ins; others to the system the file's systems declare that the package belongs to (a curated table: bt2 is bordeaux-threads, ppcre cl-ppcre, 5am fiveam; asdf:register-system-packages; the package's own name). In a package-inferred system a package is its system. A package nothing declares is an unresolved project; an in-package or a qualified symbol of one is dropped.
  • Pins: a Quicklisp dist is an immutable snapshot, so its version pins. qlfile.lock pins every project (a dist version, a git commit) and the lock's dist pins every project the qlfile does not list; without a lock a dist version (ql x 2023-10-21, ql :all) and a git :ref pin, a :tag is shown and :latest or a branch float; ocicl.csv pins by image digest. A project nothing pins floats. --resolve-depth follows the :depends-on of the systems Qlot or ocicl installed, and --online reads a dist's systems.txt for what a project depends on.

Reader conditionals are read on both sides (#+sbcl and #-sbcl alike; #+nil and #+(or) drop their form). Top-level definitions are the symbols (defun, defmacro, defgeneric, defmethod named with its specializers, defclass, define-condition, defstruct, deftype, defvar, defparameter, defconstant, defpackage, defsystem). Macros are not expanded, and OSV has no Common Lisp ecosystem, so Quicklisp projects are checked for advisories only by the commit a Qlot or ocicl git source is locked to.

Solidity, Foundry and Hardhat

Solidity contracts (.sol) are built by Foundry, whose libraries are git submodules under lib/ (or Soldeer packages under dependencies/), or by Hardhat, whose libraries are npm packages in node_modules. depphunter reads foundry.toml, remappings.txt, soldeer.lock, the .gitmodules beside a foundry.toml and the npm manifests without running solc, forge or Hardhat; what lib/, dependencies/, out/ and cache/ beside a foundry.toml, and artifacts/, cache/ and typechain-types/ beside a hardhat.config.* hold is not read:

  • Imports resolve as solc does: ./ and ../ paths are files; else the project's remappings (foundry.toml's, every profile's, then remappings.txt's; context:prefix=target applies to files under the context, the longest context then the longest prefix wins) map the path; for a prefix neither lists, the remappings Foundry infers (dep/ = lib/dep/src/ or lib/dep/) and Soldeer generates (name-version/ = dependencies/name-version/) apply. A path into a library is the library, even when its files are on disk. Otherwise an npm package a package.json declares (@openzeppelin/contracts/..., hardhat/console.sol) is that package, and a path from the project's root (src/Counter.sol, contracts/Token.sol) its file. What nothing resolves is an unresolved package named by its first segment (an npm package outside Foundry).
  • Git submodules form the Git submodules island, named by their repository (github.com/foundry-rs/forge-std) and pinned by the commit git records for them, with the .gitmodules branch as requested; a checked-out submodule's own submodules are its dependencies.
  • Soldeer packages form the Soldeer packages island: foundry.toml's [dependencies] pinned by soldeer.lock, else by an exact version or a git commit rev. A package installed into dependencies/<name>-<version>/ depends, for --resolve-depth, on its own foundry.toml and soldeer.lock entries.
  • npm packages of Hardhat projects are JavaScript's: one node for a package both a contract and a script import, versioned by the lock files.

Contracts, interfaces and libraries, with their functions, modifiers, events, errors, structs, enums, value types and constants, and a file's free functions and constants are the symbols; imports in comments, NatSpec, strings and assembly blocks are not read. OSV has no Solidity ecosystem, so Soldeer packages and git submodules are checked for advisories only by the commit they are pinned to, when their repository is on a public forge.

Nim and nimble

Nim packages come from git repositories through nimble (or Atlas); the Nimble packages island names each package as requires does (chronos, jester), or, for a requirement written as a URL, by its repository (github.com/status-im/nim-chronos). depphunter reads .nim modules, NimScript (config.nims, *.nims), .nimble files, nimble.lock, atlas.lock and nim.cfg without running the compiler, nimble or Atlas; what nimble installed into nimbledeps/, what Atlas cloned into deps/ beside a .nimble file and the compiler's nimcache/ are not read as source:

  • Imports: import, from ... import and include in every form (import a, b/c, std/[os, strutils], pkg/x, "x.nim", as, except), in every when branch, resolve as the compiler finds a module: ./x and ../x beside the importing file; another path beside it, then under the package's srcDir, then under the --paths of the nim.cfg and config.nims files of its directory and those above it; std/x and the standard library's own module names (os, tables, asyncdispatch) are a hidden Nim standard library island (in Nim's own repository, the files of its lib/). Other names, and every pkg/x, are the package of the repository nimble.develop (or a develop file it includes) develops in place, else the nimble package whose installed files have the module (nimbledeps/pkgs2/, Atlas's checkouts, nimble.paths, the nimble directory ~/.nimble or NIMBLE_DIR for the packages the manifests name: sdl2_nim ships sdl2), else the requirement their first segment names (nim-widgets is widgets), else an unresolved package of that name.
  • Manifests: every requires and taskRequires of a .nimble file (also in when branches, feature blocks and tasks) is an import of its package, and every bin an edge to its main module; nim itself is the compiler and is left out. nimble.lock (with its tasks' packages) and atlas.lock pin (the requirement shown as requested); without them == 1.2.3, a bare 1.2.3 and a #<commit> pin, a #tag is shown, neither pinned nor floating, and a range, #head, a branch or no version float. A package from a git server other than GitHub, GitLab, Bitbucket, Codeberg or sourcehut carries its URL as its origin, and one nimble.develop develops from a directory of the repository is an edge to that directory. --resolve-depth follows nimble.lock's dependencies and installed packages' .nimble files.

Top-level routines (proc, func, method, iterator, converter, template, macro), types (objects as classes, concepts as interfaces, enums), constants and variables are the symbols; imports in comments, strings and routine bodies are not read. OSV has no Nim ecosystem, so nimble packages are checked for advisories only by the commit a lock pins them to, and the official package list has no versions or dependencies to ask for --online.

Interface definitions

Protocol Buffers definitions are shared between services and languages, and the protos they import from elsewhere — Google's API annotations, validation rules, gRPC-Gateway's OpenAPI options — are dependencies no language manifest records. depphunter reads .proto files and Buf's buf.yaml, buf.work.yaml, buf.lock and generation templates (buf.gen.yaml, buf.gen.*.yaml):

  • Imports: import, import public and import weak name a file relative to an import root. The roots are those Buf's configuration declares — the directories of a buf.work.yaml, the module paths of a v2 buf.yaml, a v1 buf.yaml's own directory — and, where no Buf configuration applies, the directories the repository's build scripts give protoc with -I or --proto_path (Makefiles, shell and PowerShell scripts, justfiles, Taskfiles and CMake files that mention protoc; variables the script sets are expanded, and the scripts nearest the importer come first), then the ones protoc is usually given: the repository root, proto/, protos/, api/, src/main/proto/ and the importer's directory and its ancestors. Buf's roots come before the scripts'. Failing those, an import resolves to the only project file whose path ends in it (a copy under third_party/).
  • Well-known types: google/protobuf/*.proto (timestamp.proto, descriptor.proto and the rest that protoc and buf ship) form a hidden Protobuf well-known types island.
  • Buf Schema Registry: an import the project does not have resolves to the module buf.yaml declares in deps (or buf.lock records) that provides it, in the Buf Schema Registry island, named buf.build/owner/repository. A short table knows where common protos come from — google/api/ and google/type/ from buf.build/googleapis/googleapis, validate/ from protoc-gen-validate, buf/validate/ from protovalidate, protoc-gen-openapiv2/ from grpc-gateway, gogoproto/ from gogo — so a protoc project that declares nothing still names the module, marked unresolved; any other missing import is unresolved under its first directory.
  • Buf's files: deps, lock entries, remote plugins (buf.build/protocolbuffers/go:v1.35.1) and module inputs are packages of that island; workspace directories and module paths are edges to those directories.

Messages (nested ones as Outer.Inner), enums, services, their rpc methods (Service.Method), extend blocks, oneofs and the package become the file's symbols. A type used from another file needs no edge of its own: protobuf requires importing the file that declares it. Only buf.lock's commit, or a commit given as the ref, pins a module. With --online a module's own dependencies are read from the registry its name carries (buf.build, or a private Buf Schema Registry this machine's buf credentials name); a remote plugin depends on nothing and is not asked.

Shell scripts

Build, CI, install and deployment scripts decide what runs as much as any manifest, and they call each other. depphunter reads shell scripts — .sh, .bash, .zsh, .ksh, .bats, Oh My Zsh's .zsh-theme, the shells' startup files (.bashrc, .zshrc, .profile and the rest), direnv's .envrc, and any file without an extension whose #! line runs sh, bash, zsh, dash, ksh, mksh or ash, directly or through env:

  • Sourced files: source and ., with the path worked out from what the file says — literals, variables it assigned earlier, and the idioms for the script's own directory: $(dirname "$0"), ${BASH_SOURCE%/*}, SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)", zsh's ${0:A:h}. $(git rev-parse --show-toplevel) is the repository root. A bare relative path is relative to the working directory, which a script cannot know; it is looked up beside the script, then at the root.
  • Scripts it runs: a command that is a path (./build.sh, "$DIR/deploy", also after exec, env, sudo or time), and the script an interpreter is handed (bash x.sh, python3 tools/gen.py, node x.js).
  • Paths below the environment: "$PLUGIN_PATH/common/functions", where the variable comes from whatever runs the script, resolves to the one project file ending in common/functions (two elements at least).
  • direnv and bats: source_env, source_up and dotenv in an .envrc; load in a bats test.
  • Installed packages: pip install (and python -m pip, uv, pipx), npm install/pnpm add/yarn add, go install pkg@version, cargo install and gem install add packages to the PyPI, npm, Go modules, crates.io and RubyGems islands the manifests use, under the same pinning rules, so vulnerability lookups and private patterns cover them; pip install -r requirements.txt is an edge to that file.

Functions, bats tests, aliases, and the exported, read-only and upper-case variables a script sets at its top level become its symbols. Nothing is run: a path under ~ or $HOME, one computed in a loop or by eval, or from a variable another file sets is not followed, and packages installed with apt-get, apk, brew or another system package manager are not read, as no island holds them.

Documentation

A README that links to CONTRIBUTING.md depends on that file, and one that links to internal/server/server.go depends on that. Both break when the target is moved, and no package manifest records the relationship, so the links are placed on the map alongside the imports.

A link to a file or a directory in the repository becomes an edge from the document to it, drawn as any other dependency is, and the headings become the file's symbols: a document expands into its sections as a source file expands into its functions. Inline links, reference definitions, autolinks and the href and src attributes of raw HTML are all included, since a badge is also a link. A link within a fenced block or a code span is not, since it is rendered as text rather than followed.

There is no island for external hosts. A link to https://example.test is not a dependency the map can characterize, and a legend of third-party domain names would add nothing.

A link that leads nowhere becomes a finding instead:

What is wrong Reported as
the file or directory is not there link/missing-file
the heading it names is not in it link/missing-anchor
the reference was never defined link/undefined-reference
the host says the page is gone link/gone (--online)

The first three require nothing beyond the repository, so they are checked on every run and are exact: a path either names something on disk or it does not, and a fragment either matches a heading of the file it points at or it does not. A link to a file that exists but is absent from the map — ignored, excluded or generated at build time — is not broken; it simply produces no edge.

The fourth requires a third-party server and is therefore performed only under --online. Links are checked with the same credentials the indexes use, host by host, so that a link into a private repository or an internal wiki is checked rather than reported missing on the strength of an anonymous 404. The check is otherwise deliberately conservative: a link is reported when a host states that the page is gone, that is 404 or 410, and not when the host refuses a robot, rate-limits, times out or fails. Those are the responses a checker receives from Cloudflare and from GitHub's own bot rules, and treating them as link rot would produce a finding for every link that functions correctly in a browser. Links that could not be checked are reported in the log rather than placed on the map. Answers are cached for one day.

Two categories of Markdown are excluded from all of the above, because a finding against either would be a defect nobody is expected to correct: vendored documentation, whose links point at the parts of its own repository that vendoring does not copy, and fixtures under testdata, which are incorrect by design.

--no-links disables the whole of it.

Symbol references

Imports establish which files depend on which. Under --lsp, depphunter additionally queries language servers for which symbols use which. It uses the servers found on PATH, and the go install locations for gopls:

Language Server
Go gopls
JavaScript / TypeScript typescript-language-server
Python pyright-langserver, basedpyright-langserver or pylsp
Rust rust-analyzer
Java jdtls
Kotlin kotlin-language-server
Scala metals
C# csharp-ls
F# fsautocomplete --adaptive-lsp-server-enabled
C / C++ / Objective-C clangd (CUDA, Metal and OpenCL headers too)
PHP intelephense or phpactor
Ruby ruby-lsp or solargraph
Swift sourcekit-lsp
Dart dart language-server
Elixir elixir-ls (or language_server.sh), lexical or nextls
Erlang elp or erlang_ls
R R --slave -e languageserver::run()
Haskell haskell-language-server-wrapper or haskell-language-server
Terraform / OpenTofu terraform-ls serve or tofu-ls serve
Jsonnet jsonnet-language-server
CUE cue lsp
Dhall dhall-lsp-server
Puppet puppet-languageserver --stdio
Rego regal language-server
GLSL glsl_analyzer
HLSL shader-language-server
WGSL / WESL wgsl-analyzer or wgsl_analyzer
Protocol Buffers buf lsp serve, bufls serve or protols
Shell (sh, Bash, bats) bash-language-server start
CMake neocmakelsp --stdio or cmake-language-server
Lua lua-language-server
Luau luau-lsp lsp
Perl perlnavigator --stdio, pls or perl -MPerl::LanguageServer -e Perl::LanguageServer::run
OCaml ocamllsp
Julia julia --startup-file=no --history-file=no -e "using LanguageServer; runserver()"
Zig zls
Clojure clojure-lsp
Bazel (Starlark) starpls server, bazel-lsp or bzl lsp serve
Nix nil or nixd
Gleam gleam lsp
Elm elm-language-server --stdio
PureScript purescript-language-server --stdio
Crystal crystalline
D serve-d
Haxe haxe-language-server
Ada ada_language_server
Racket racket -l racket-langserver
Common Lisp cl-lsp
Solidity nomicfoundation-solidity-language-server --stdio or solidity-ls --stdio
Nim nimlangserver or nimlsp
Fortran fortls

The servers run in the background once the map is displayed — gopls requires approximately 7 s for this repository — within the budget set by --lsp-timeout; results are cached until the map's files, symbols or imports change, or a different set of language servers is installed. Servers that index slowly, rust-analyzer, jdtls, metals, clangd and the PHP, Ruby, Swift, Elixir, Erlang, Haskell, Julia, Clojure, F# and Nim servers in particular, may answer before indexing has finished, so a first run can report fewer references than a later one. The legend's Imports / References switch then determines what the selection arcs and the side panel show: for a function, what it uses and what uses it. The JSON and GraphML exports include the reference edges.

Languages

Ecosystem Imports resolved through Islands
Go every go.mod (multi-module, local replace) Go modules, Go standard library
JavaScript / TypeScript relative paths, tsconfig/jsconfig paths, workspaces, package.json + package-lock.json / yarn.lock / pnpm-lock.yaml / bun.lock; also the scripts of Vue, Svelte and Astro components, and SvelteKit's $lib npm, Node.js built-ins
Python relative imports, src/ layouts, PYTHONPATH and configured source roots (below), requirements files, setup.cfg, literal setup.py lists, pyproject.toml, Pipfile, poetry.lock/uv.lock/pdm.lock/Pipfile.lock, installed environments (below) PyPI, Python standard library
Rust the module tree (crate::, self::, super::, mod x;), workspace and path crates, Cargo.toml (renamed and workspace dependencies) + Cargo.lock crates.io, Rust standard library
Java source files by package path (any source root), pom.xml (properties, dependency management), Gradle scripts (string and map notation), version catalogs and lock files, sbt builds; imports to the declared group:artifact shipping the package Maven, Java standard library
Kotlin source files by the package they declare (any directory; Java files by path), the Java manifests Maven, Kotlin and Java standard libraries
Scala source files by the package they declare (any directory; Java files by path), build.sbt (%, %% with the scalaVersion suffix, versions held in a val), the Java manifests Maven, Scala and Java standard libraries
C# namespaces to project folders (RootNamespace + folder), PackageReference, Directory.Packages.props, and the packages of .fsproj files, Paket and packages.lock.json through the NuGet reader shared with F# NuGet, .NET base library
F# open and qualified names to the files declaring the namespace, module or type, earlier in the .fsproj compile order or in a referenced project; <Compile>, <ProjectReference>, <PackageReference>; paket.dependencies / paket.lock / paket.references (groups, GitHub, git and HTTP files); #load and #r "nuget: ..." in scripts NuGet, .NET base library, Paket git, GitHub and HTTP sources
C / C++ / CUDA / Metal #include beside the includer, the include paths of compile_commands.json (-I, -iquote, -isystem, /I), include/ and src/, and a unique project file ending in the included path; libraries by vcpkg.json, conanfile.txt, conanfile.py and conan.lock, and by the content CMake fetches; CUDA (.cu, .cuh), Metal and OpenCL C kernels read the same way vcpkg, Conan, C/C++ external, C and C++ standard libraries, system headers, CUDA Toolkit, OpenCL headers, Apple SDKs
CMake add_subdirectory, include of files and of modules on CMAKE_MODULE_PATH, target sources, configure_file templates, presets; find_package as the includes resolve, FetchContent, ExternalProject, CPM.cmake, pkg_check_modules vcpkg, Conan, C/C++ external, fetched content, pkg-config, CMake modules
PHP use statements (grouped, function, const) and fully qualified names in code, same-namespace extends/implements, require/include of spelled-out paths; project files by what they declare and composer.json PSR-4/PSR-0; packages by the autoload prefixes of composer.lock or installed.json Packagist, PHP standard library
Ruby require/require_relative/load/autoload of spelled-out paths on a guessed load path (lib, test, spec, gemspec require paths, path gems); gems by Gemfile, gemspecs and Gemfile.lock, whose gem lines are imports; Rails constants by Zeitwerk naming RubyGems, Ruby standard library
Swift import (every #if branch); Package.swift targets to their directories, else a directory named after the module; packages by Package.swift, Package.resolved and Xcode's project.pbxproj, products first, whose .package lines are imports; types used across a module's files Swift packages, Swift standard library, Apple SDKs
Objective-C #import/#include as C includes (beside the file, compile_commands.json, Xcode header search paths, include/ and src/, a unique file ending in it), @import; framework headers and modules to pods by Podfile, Podfile.lock and podspecs, or to Carthage by Cartfile and Cartfile.resolved, whose entries are imports; headers under Pods/ and Carthage/ to their dependency CocoaPods, Carthage, Apple SDKs, C/C++ islands
Dart import, export (every configurable URI), part and part of; relative URIs, package: URIs to a package's own lib/, path dependencies, pub workspace members and melos packages; packages by pubspec.yaml and pubspec.lock, whose dependencies are imports pub, Dart SDK libraries, Flutter SDK
Elixir / Erlang Elixir alias/import/require/use and every module reference, Erlang remote calls, -behaviour, -include/-include_lib; modules to the files defining them (umbrella apps too); packages by mix.exs, rebar.config, mix.lock and rebar.lock, whose dependencies are imports Hex, Elixir standard library, Erlang/OTP
R library/require/requireNamespace/loadNamespace, pkg::, pacman, box::use, roxygen @import; source() paths, knitr children; calls to a package's own functions across its files; packages by DESCRIPTION, NAMESPACE, renv.lock and packrat.lock, whose dependencies are imports CRAN, Bioconductor, R base packages
Haskell import (every CPP branch, PackageImports, {-# SOURCE #-}); modules to the files whose headers declare them (the importer's package, its project's and its dependencies' local packages), Happy/Alex sources by path; packages by .cabal, package.yaml, cabal.project, stack.yaml, the freeze file, stack.yaml.lock and plan.json, whose dependencies are imports Hackage, GHC libraries
Lua / Luau / Teal require (a string, pcall(require, …), Luau paths and .luaurc aliases, Roblox instances), dofile/loadfile; modules to files by a rockspec's build.modules, else ?.lua/?/init.lua under the file's directories and their lua/, src/ and lib/ and .luarc.json's roots; Roblox instances through Rojo projects; rocks by rockspecs and luarocks.lock, Wally packages by wally.toml and wally.lock, whose dependencies are imports LuaRocks, Wally, Lua standard library, Lua host runtimes
Perl use/no/require (and in a string eval), use parent/use base, Moose's with/extends, Corinna's :isa, require/do of files; modules to Foo/Bar.pm under use lib (literal, FindBin, __FILE__, Mojo::File and Path::Tiny chains), the distribution's lib/ and t/lib, each lib/ above the file, else a file declaring the package; distributions by cpanfile.snapshot, cpanfile, META.json/META.yml, Makefile.PL, Build.PL and dist.ini, whose requirements are imports CPAN, Perl core modules
OCaml module paths (Foo.bar, open, include, module M = Foo, functor arguments), #require; modules to the files of the importer's dune library, executable or test (include_subdirs, modules), to what an opened module of the project declares and to local libraries (Lib.Module of a wrapped one); libraries and packages by dune files, dune-project, *.opam, *.opam.locked and dune.lock/, whose dependencies are imports opam, OCaml standard library
Julia using/import (relative .Sub/..Parent through the include graph), include/includet (joinpath(@__DIR__, …)); the package's own src/Name.jl and submodules, local packages by UUID, [sources] or a manifest path; packages by Project.toml ([deps], [weakdeps], [extras], [compat], [extensions], [workspace]) and Manifest.toml, whose entries are imports Julia, Julia standard library
Zig @import of files (@embedFile too), std/builtin, root (the compilation's root source file) and module names to what build.zig wires (b.addModule, b.createModule, .imports, addImport, a dependency's .module()), else the build.zig.zon dependency of that name; b.path() in build code; @cInclude as C includes; packages by build.zig.zon (named by URL, pinned by .hash), whose dependencies are imports Zig, Zig standard library, C/C++ islands
Clojure ns :require/:use/:import, top-level require/import/load (prefix lists, every reader-conditional branch) to files under the source paths of deps.edn, project.clj, shadow-cljs.edn, bb.edn (a.b-c is a/b_c.clj), :local/root projects, Clojure's own namespaces, artifacts the manifests declare (a table and naming rules), JDK classes; npm strings in ClojureScript Maven, Clojure standard library, JDK, npm, Node.js built-ins
Bazel load() and label attributes (srcs, hdrs, deps, data, ...) to files and to the BUILD file of each package, glob() expanded within the package; other repositories by MODULE.bazel (bazel_dep, overrides, MODULE.bazel.lock), WORKSPACE and .bzl repository rules (http_archive, git_repository, local_repository, go_repository), and the hub repositories of rules_jvm_external, rules_python, Gazelle, rules_js and rules_rust Bazel modules and repositories, Maven, PyPI, Go modules, npm, crates.io
Nix import, callPackage and NixOS module imports of paths (a directory is its default.nix), other path literals; flake inputs (github:, gitlab:, git+https:, tarballs, path:, registry names, follows) pinned by flake.lock, inputs.x; <nixpkgs>; niv and npins sources; builtins.fetchTarball/fetchGit/fetchTree; nixpkgs attributes in buildInputs, nativeBuildInputs, packages, systemPackages Nix flakes and sources, Nixpkgs
Gleam import (with unqualified lists and aliases) to the src/, test/ and dev/ modules of the package, of path dependencies and of build/packages when on disk; gleam/* to gleam_stdlib, gleam_erlang, gleam_otp and the like, other modules to the package their leading segments name; @external Erlang modules (compiled Gleam modules, .erl files, OTP, packages) and JavaScript files or npm packages; packages by gleam.toml and manifest.toml, whose dependencies and packages are imports Hex, Erlang/OTP, npm
Elm import to A/B.elm under the source directories of every elm.json listing the file (an application's source-directories, a package's src/, tests/ for elm-test), kernel modules to their .js; other modules to the package whose installed elm.json in ELM_HOME exposes them, elm/core's modules to elm/core, else a curated module table or the listed package the module spells; packages by elm.json, whose dependencies are imports Elm packages
PureScript import to the file declaring the module in the importing package's sources (src/ and test/ of a spago.yaml package, a spago.dhall's sources), then its workspace's; foreign import to the .js beside the module; Prim modules to the compiler's built-ins; other modules to the package spago installed in .spago/ that provides them, else a curated module table or the listed package the module spells; packages by spago.yaml, spago.lock, spago.dhall, packages.dhall and bower.json, whose packages are imports PureScript packages, PureScript built-ins
Crystal require of relative paths and globs (./dir/*, ./dir/**) to files; by name through lib/ (the installed shard), the project's own src/ and shard name, the standard library, then the shards of shard.yml / shard.lock / shard.override.yml, whose dependencies and targets' main: files are imports Crystal shards, Crystal standard library
D import to a/b.d or a/b/package.d under the importPaths/sourcePaths (source/, src/) of the file's dub package and the repository packages it depends on, else its ancestors; import("file") under stringImportPaths (views/); other modules to the package dub fetched, a declared package they spell or a curated table; dub.json, dub.sdl, dub.selections.json and single-file recipes, whose dependencies are imports dub packages, D runtime and standard library
Fortran use to the file that defines the module (any build system, case-insensitive), submodules to their parent; include/#include beside the file or in include-dir; other modules to the package fpm fetched, a declared package they spell, the C library (MPI, HDF5, NetCDF) or a curated table; fpm.toml dependencies are imports fpm packages, Fortran external modules, Fortran intrinsic modules
Haxe import, using and qualified names in code to the module's file, found by the package every module declares (sub-types to their module, a.b.* to every module of the package); others to the standard library, the library haxelib or lix installed that has them, a declared library they spell or a curated table; .hxml, haxelib.json, lix pins and Lime Project.xml files, whose libraries, class paths and main classes are imports haxelib libraries, Haxe standard library
Ada with clauses to the file declaring the unit (the source directories of the file's projects first), a body to its spec, separate (P) to the parent body, a child to its parent; others to the predefined units, the crate Alire fetched that has them, a declared crate they spell or a curated table; .gpr files' with, Source_Dirs and Main, and alire.toml's dependencies, pins and project files Alire crates, Ada predefined units
Racket #lang, require in every form (only-in, for-syntax, submod, lib, file, planet, multi-in), require/typed, include, load, Scribble's @(require ...) and include-section; relative paths to files, collection paths to the repository's collections (packages' info.rkt, multi-collection packages, collects/), else the base collections, a curated table or the info.rkt deps they spell; info.rkt deps and build-deps Racket packages, Racket base collections
Common Lisp .asd defsystem components (modules and :pathname) and :depends-on (local .asd, secondary and package-inferred systems, the implementation, else the Quicklisp project), defpackage :use/:import-from/:local-nicknames, in-package and pkg:sym to the file defining the package or the declared system it belongs to, ql:quickload, asdf:load-system, require, load; qlfile, qlfile.lock and ocicl.csv entries Quicklisp projects, Common Lisp built-ins
Solidity import in every form to files by relative path, the remappings of foundry.toml and remappings.txt (with contexts) and those Foundry and Soldeer infer, npm packages a package.json declares, else the project's root; libraries under lib/ (git submodules by .gitmodules, pinned by the recorded commit) and dependencies/ (Soldeer, pinned by soldeer.lock); foundry.toml remappings and dependencies, remappings.txt, soldeer.lock and .gitmodules entries Soldeer packages, Git submodules, npm
Nim import, from, include (std/[a, b], pkg/x, every when branch) to files beside the importer, under the package's srcDir and nim.cfg/config.nims --paths; std modules to the standard library (Nim's own lib/ in its repository); else the nimble package whose installed files (nimbledeps/, Atlas's deps/, nimble.paths, ~/.nimble) have the module, else the requirement its first segment names; .nimble requires, taskRequires, bin, nimble.lock and atlas.lock entries Nimble packages, Nim standard library
PowerShell using module, Import-Module, dot-sourced and &-invoked scripts ($PSScriptRoot), #Requires -Modules, module manifests (RequiredModules, RootModule, NestedModules), Install-Module and Install-PSResource commands, PSDepend files (requirements.psd1, *.depend.psd1) PowerShell Gallery, built-in modules
CI pipelines GitHub workflows and composite actions (uses:, reusable workflows, container:, services:), GitLab pipelines (every include: form, components, image:, services:) GitHub Actions, GitLab CI, Container images
Protocol Buffers import (public, weak) under the import roots of buf.work.yaml and buf.yaml (v1 and v2), else the -I roots build scripts give protoc, the repository root, proto/, protos/, api/, src/main/proto/ and the importer's directories, else a unique project file ending in the path; modules by buf.yaml deps and buf.lock and a table of common protos; buf.gen.yaml remote plugins Buf Schema Registry, Protobuf well-known types
Terraform / OpenTofu module sources to local directories, registry and remote modules; required_providers, provider blocks and resource type prefixes to providers, pinned by .terraform.lock.hcl; references to what other files of the module declare; file()/templatefile() paths; Terragrunt source, dependency and find_in_parent_folders() Terraform modules, Terraform providers
Jsonnet import, importstr, importbin relative to the importer, then the vendor/ and lib/ of the jsonnet-bundler projects above it, JSONNET_PATH and the importer's ancestors; files jb installed (full path or legacy link) and paths jsonnetfile.json names to their dependency (repository + subdir), local sources to their files; jsonnetfile.json and jsonnetfile.lock.json entries jsonnet-bundler packages
CUE import of the module's own packages (every file of the package in its directory, by the module path of cue.mod/module.cue), CUE's builtin packages, cue.mod/gen and cue.mod/usr to the Go module go.mod requires (or the Go standard library, or the module's own Go package), cue.mod/pkg vendored modules, module.cue deps by module path CUE modules, CUE standard library, Go modules, Go standard library
Dhall imports: relative paths to their files (as Text, as Location, every ? branch), URLs to the Prelude, the repository GitHub, GitLab or jsDelivr serve, else host and path up to the version, pinned by sha256:; absolute, home and env: imports dropped Dhall packages
Puppet include/require/contain, class { }, inherits, Class[], defined and custom types, x::y(), X::Y types, template(), epp(), file(), puppet:///modules/ to the autoloader's file of a repository module, else the module metadata.json, .fixtures.yml or the Puppetfile declares (Forge slug or git repository), else r10k's installed modules/; Puppetfile, metadata.json and .fixtures.yml entries Puppet modules
Rego import data.a.b and data.a.b... references (also through imported names) to every file of the longest package the path names (none: policies import only the repository's own)
Shaders (GLSL, HLSL, WGSL) #include as C headers of the project (then parent directories, ignoring case), Unreal virtual paths (/Engine/, /Plugin/Name/), naga_oil #import and #define_import_path, WESL import (package::, super::), crates the Cargo manifests declare (Bevy's bevy_* through bevy) Unreal Engine shaders, crates.io
Shell scripts source/. and scripts run by path or interpreter, with $(dirname "$0"), ${BASH_SOURCE%/*}, SCRIPT_DIR variables, zsh's ${0:A:h} and git rev-parse --show-toplevel evaluated; direnv source_env/source_up/dotenv, bats load; packages installed with pip, npm, pnpm, yarn, go install, cargo install and gem install PyPI, npm, Go modules, crates.io, RubyGems
Dockerfile / Compose FROM, COPY --from, RUN --mount from= and # syntax= with ARG defaults expanded and stages told apart; Compose image:, and build: to the Dockerfile in the repository, ${VAR} from the .env file beside it, include: and extends: of local files Container images
Markdown links to files and directories in the repository (inline, reference, autolink, and the href and src of raw HTML); headings become the file's symbols (none: a link is not a package)

Python imports of the repository's own modules are looked up under the directories of its manifests and their src/, and also under the roots projects declare elsewhere, where they lie inside the repository and hold Python files: the PYTHONPATH depphunter runs with (relative entries against the repository root); the PYTHONPATH of .env files (read like python-dotenv, with ${workspaceFolder} and ${PWD} as the repository root, also from a git-ignored .env; no other value of the file is kept); .vscode/settings.json's python.analysis.extraPaths, python.autoComplete.extraPaths, python.envFile and the integrated terminal's PYTHONPATH; Pyright's and basedpyright's extraPaths and executionEnvironments (the latter for the files under their root); pytest's pythonpath; mypy's mypy_path; and the source directories of setuptools (package-dir, packages.find where), Poetry (from), Hatch, PDM and maturin. A project's own roots are tried first, deepest first, then the configured ones (the process's PYTHONPATH first, then deeper files first), then the repository root. --explain notes each root added and what added it (import-root).

Python packages that no index has - an in-house package installed from a directory, a wheel file or a Git repository - are resolved from what a Python environment has installed. The environment is the interpreter named with --python (DEPPHUNTER_PYTHON, or python: in the user's own configuration file), otherwise the activated virtual environment (VIRTUAL_ENV), otherwise the project's own .venv or venv; the per-user site directory (pip install --user) is not read. Its site-packages directories are read, along with the base interpreter's when the virtual environment includes system site-packages; the interpreter itself is never run. A distribution counts only when its metadata records that it was installed from somewhere other than an index (direct_url.json, PEP 610). Such a package takes its name and version from its installed metadata, and its dependencies from its Requires-Dist. It is treated as private: it is never named to an index or to OSV, it is attributed to where it was installed from rather than to any index (so it is never marked ⚠ index), and the side panel says where that was. An import that an index-installed distribution provides but no manifest declares remains unresolved, but under that distribution's name. The extension has no setting for this; pass --python through depphunter.args.

Java imports name packages rather than artifacts, so each is matched to the declared Maven artifact that ships it, and the package is named group:artifact — as POMs, Maven Central, OSV and Trivy name it, and as the Clojure and Bazel plugins do, so a library several builds reach is one building. The longest package prefix wins: one from a table of well-known libraries (com.google.common is com.google.guava:guava, org.apache.commons.io is commons-io:commons-io, okhttp3 is com.squareup.okhttp3:okhttp), one the artifact's name suggests (org.springframework:spring-context gives org.springframework.context, com.fasterxml.jackson.core:jackson-databind gives com.fasterxml.jackson.databind, cats-effect gives cats.effect), or its group. Among artifacts matching equally, the one whose group names the import's root package wins (liquibase is org.liquibase's, not an extension's named after it), then the one whose name the import spells (io.ktor.client.engine.cio is ktor-client-cio), then the family's main one (spring-boot, a -core). A table artifact the build does not declare still matches when another of its group is declared, since it comes with it: com.fasterxml.jackson.annotation is jackson-annotations beside jackson-databind, at its version, and a Spring Boot starter brings spring-boot. A package the table gives an artifact that is not declared is no other artifact's (com.google.common.jimfs is com.google.jimfs:jimfs, not Guava's). An import nothing declared matches is unresolved, named after the table's artifact (javax.servlet:javax.servlet-api) or guessed from its package (net.sf.saxon.s9api becomes net.sf.saxon:saxon).

Vue, Svelte and Astro components are part of the JavaScript/TypeScript ecosystem: the code in each component's <script> blocks (<script setup>, Svelte's module script) and in an Astro component's --- frontmatter is read as JavaScript or TypeScript, by its lang, and resolved like any other; a <script src> is an import too, and a component imported from a script, as ./Button.vue, is an edge to that file. Scripts that are markup rather than the component's code - inside a Vue <template> or <svelte:head>, or an Astro script left inline - are not read, nor is @import in a <style>. Each component is a symbol named after its file, beside its functions and constants. A .ts file that opens with an XML declaration or document type is a Qt Linguist translation, not TypeScript: it is labeled XML and not parsed.

Kotlin and Scala share Java's resolution: the same manifests, the same Maven islands, the same pinning rule, and the JDK. A Kotlin or Scala file need not sit in a directory named after its package, so each is found by the package it declares and its top-level definitions, told by their nesting rather than their column (a Scala package block's are in its own package); this index serves all three languages, so a Java class may import a Kotlin one and the reverse. kotlin.* and scala.* form their own standard-library islands; kotlinx.* and the modules split from the Scala library, such as scala.xml, are ordinary Maven dependencies. Scala imports are relative, so each is tried against the packages around the file first, then against scala._ unless a dependency owns that root (io.circe is not scala.io); an import of a value's members (import builder._) is not a dependency and is dropped, as is an import of a class in the default package, which Gradle scripts declare. sbt's %% appends the Scala binary version to an artifact's name, as Maven Central publishes it: with scalaVersion := "3.3.3", "org.typelevel" %% "cats-effect" is org.typelevel:cats-effect_3 (a val holding the version works too; the file's own scalaVersion, else the root build's). Without a scalaVersion the name stays as written, and %%% is read as %%, since the Scala.js or Native platform suffix depends on the project.

C and C++ are one plugin, since their files include each other: .c files are read as C and every other extension (.h, .cc, .cpp, .cxx, .c++, .hpp, .hh, .hxx, .h++, .ipp, .inl) as C++, which covers nearly all C headers too. Includes are read the way the preprocessor reads them, line by line, and resolved in the compilers' order: #include "x" first beside the including file; then the include directories of a compile_commands.json (at the root or in a build*/ or cmake-build-*/ directory, read from disk even when git ignores it), the file's own entry or, for a header, every entry's; then include/, src/ and the root; and last the one project file whose path ends in the include (foo/bar.h in libs/foo/bar.h), or the nearest one to the includer. Projects often include their own headers with <>, so that form is looked up the same way, except that a standard or system header is taken from the project only through the compilation database. What the project does not have is a C or C++ standard header (<stdio.h>, <vector>), a system header (POSIX, sys/*, Windows, Apple frameworks, intrinsics), or else a third-party library named after its first directory (<boost/asio.hpp> is boost, <zlib.h> is zlib), shown unresolved; a quoted bare name the project lacks, such as a generated config.h, is dropped. Only #if 0 and #if 1 are evaluated: the includes of every other branch count, whatever the platform, and macros are not expanded, so #include CONFIG_H is not followed.

A library that a vcpkg or Conan manifest declares takes the header's place under its package: the manifests in the including file's directory and those above it are searched, the nearest first (or, for a file under no manifest, every manifest in the project, the shallowest first — sibling directories are often built under one), for a package named like the header's library (ignoring case and - against _), a known alias of it (gtest/googletest, nlohmann/nlohmann-json, Eigen/eigen3, SDL2, GLFW/glfw3, google/protobuf, absl/abseil and a few more), or its name after lib (<curl/curl.h> is Conan's libcurl); a Boost header belongs to the port of its directory (<boost/asio.hpp> to boost-asio) before boost, and a Qt module's directory (<QtCore/QString>) to its own port before qtbase and qt. vcpkg's vcpkg.json is read for its dependencies and those of its features (names or objects with version>=; platforms are not evaluated) and overrides; Conan's conanfile.txt for its [requires] and tool sections, a conanfile.py for the string literals given to self.requires() and its kin or assigned to requires and tool_requires, and a conan.lock beside them in either Conan 1 or Conan 2 form, whose libraries count as declared since the build installs them; a reference keeps its user, channel and the recipe revision a lock pins. The recipe is not run, so a reference built at run time (an f-string that substitutes) is not seen and a conditional one counts whatever the condition. With --online, a Conan package's own requirements are the requires of its recipe on the Conan remotes (see Package indexes), read the same way. A vcpkg port pins only by an override: a builtin-baseline fixes versions through the registry's history, which the repository does not carry, so a port without a version is then neither pinned nor floating, while without a baseline it floats. A header no manifest's package claims goes, by the same names, to content the CMake build fetches (below). Everything else stays in C/C++ external, without a version.

CMake builds — CMakeLists.txt, *.cmake, *.cmake.in package configuration templates and CMakePresets.json/CMakeUserPresets.json — tie the build to the code and the libraries. add_subdirectory(dir) is an edge to dir/CMakeLists.txt; include() of a path to that file, of a module name to Name.cmake on CMAKE_MODULE_PATH (as set() and list(APPEND) in the file and the CMakeLists.txt above it build it), else to a module CMake ships (FetchContent, GNUInstallDirs, CTest, the Check* modules) in a hidden CMake modules island; the sources of add_library(), add_executable() and target_sources() and configure_file()'s template are edges to those files; presets' include and toolchainFile too. Paths are evaluated from the file's variables, those the CMakeLists.txt files above it set, and CMAKE_CURRENT_SOURCE_DIR, CMAKE_CURRENT_LIST_DIR, CMAKE_SOURCE_DIR, PROJECT_SOURCE_DIR and <Project>_SOURCE_DIR; conditions and loops are not evaluated, and binary-directory, environment and configure-time values are not followed. find_package(X) lands where an #include of X's headers lands, so the build file and the sources meet on one node: a vcpkg or Conan package the manifests declare, else content the build fetches under the header's name, else the C/C++ external library named after the include directory (ZLIB is zlib, nlohmann_json is nlohmann, Eigen3 is Eigen, each Boost and Qt component on its own: Qt6 Widgets is QtWidgets). Before that fallback come a project of that name in the repository, the project's own FindX.cmake, and content it fetches under that name; CMake's find modules for tools and the platform (Threads, OpenMP, Python3, Git, Doxygen, CUDAToolkit) are CMake modules. FetchContent_Declare(), ExternalProject_Add() and CPM.cmake's CPMAddPackage() ("gh:owner/repo@1.2.3" or keywords) are packages of the CMake fetched content island named by repository or download URL (github.com/google/googletest; a GitHub release asset or archive by its repository and ref), and FetchContent_MakeAvailable(name) links to the declaration. The sources' includes of fetched headers land on it too: an include's directory or bare header name (<doctest/doctest.h>, <magic_enum.hpp>, <nlohmann/json.hpp>) matched, as for vcpkg and Conan, against the name the content is declared under, its repository's name and owner-repository, the declaration nearest above the source first. A GIT_TAG commit or a URL_HASH pins; a tag is shown but can be moved, so it neither pins nor floats; a branch or no tag floats. pkg_check_modules() modules are the pkg-config modules island. Targets, functions, macros, options, cache variables, projects and presets become symbols.

PHP files (.php, .phtml, .inc) are read for their use statements, grouped ones (use App\{A, B as C}) and use function/use const included, and for the classes code names: those of new, X::, extends, implements, a trait use and catch, qualified as PHP qualifies them (the file's namespace, then its aliases), and fully qualified function calls (\f()). A require or include counts when its path is spelled out: string literals, __DIR__, dirname(__FILE__) and dirname(__DIR__, n) joined with .; a relative path is looked up beside the file, then at the project roots, as PHP's include path would. A name resolves to the project file that declares it - read from every PHP file's namespace and definitions, so classmaps and projects without Composer work - or by the PSR-4 and PSR-0 rules of each composer.json (a path repository's package counts as the project's own); then to PHP itself, grouped by extension (Exception is core, DateTime is date, PDO is pdo); then to the Composer package whose autoload prefix it falls under, which composer.lock records for every installed package (Symfony\Component\HttpFoundation\ is symfony/http-foundation), or vendor/composer/installed.json where there is no lock. The platform requirements (php, ext-*, lib-*) are not packages. A namespace no prefix claims goes to the declared package its segments name (PHPUnit is phpunit/phpunit, Psr\Http\Message is psr/http-message), or else is shown unresolved under the name its first two segments suggest. Composer reads a bare version as exact, so 1.2.3 pins even without a lock; the lock pins everything it holds and gives --resolve-depth its edges. A global function nobody here defines (a framework helper loaded by a files autoload) is dropped, since its package cannot be told from its name.

Ruby files (.rb, .rake, .gemspec, .ru, and Gemfile, Rakefile, Guardfile, Capfile) are read for require, require_relative, load and autoload whose path is spelled out: string literals, __dir__, File.dirname(__FILE__), File.expand_path(path, base), File.join and +. A require is looked up on the load path Bundler, Rake and RSpec would set up: the lib, test and spec directories of the file's directory and those above it, every gemspec's require paths, the lib of path gems, and a Rails application's app/*; then in Ruby itself (json, set, net/http, yaml as psych), unless the project declares or locks that library as a gem, and then to a gem: active_support is activesupport, rails is railties, rspec/core is rspec-core, and a path nothing declares is shown unresolved under its first segment. A Gemfile's gem lines and a gemspec's add_dependency calls are imports of what they declare, since a Rails application's gems are loaded by Bundler.require and seldom required by name; a gem the repository builds itself is its gemspec. Gemfile.lock pins every gem it holds (a git gem by its revision) and gives --resolve-depth its edges; without it a bare 1.2.3 or = 1.2.3 pins. In a Rails application (config/application.rb), constants resolve to the files Zeitwerk loads them from - every app/* directory and its concerns are roots, lib too under autoload_lib - trying the modules around the reference innermost first, so models, controllers and concerns are connected without a require.

Swift files are read for import declarations, with every branch of an #if block read, since each is built on some platform. A module the project builds resolves to its sources' directory: a target of any Package.swift at its path: or Sources/<name> (Tests/<name> for tests), or, for an Xcode project, whose targets are not read, a directory named after the module. The toolchain's modules (Foundation, XCTest, Glibc) form a Swift standard library island and Apple's frameworks (SwiftUI, UIKit, Combine) an Apple SDKs island. Any other module is looked up among the packages the project declares: the product a target takes from a package (.product(name: "NIOCore", package: "swift-nio"), or an Xcode product dependency), a table of well-known modules (Logging is swift-log), then the package whose name the module's name spells (Collections is swift-collections); a module nothing declares is shown unresolved. A package is named by its URL without scheme or .git (github.com/apple/swift-nio), the name OSV and Trivy use. Package.resolved (beside Package.swift or in an Xcode project's xcshareddata/swiftpm) pins the packages it holds, and a Package.swift's .package lines are imports of what they declare. Since a module's files see each other's declarations without imports, the type names a file uses connect it to the file of its module, or of a project module it imports, that declares them. A module no SwiftPM manifest provides may be a pod or a Carthage framework: an app's Podfile or Cartfile is read for it too. A registry package (.package(id: "mona.LinkedList")) is named by its identity; with --online, --resolve-depth reads its Package.swift from the registry SwiftPM's registries.json maps its scope to. Swift is read by a small scanner: the tree-sitter grammar failed on about one file in seven and spent up to three seconds on each of them.

Objective-C sources (.m, .mm, and .h files that show Objective-C in their first lines: #import, @interface, @protocol, @class) share the C/C++ plugin's include reading and resolution, #import included; a .h file without those stays C or C++, and a .m file without a preprocessor line, a // comment or an Objective-C keyword is MATLAB (or Mercury, by its :- declarations) and is not read. An include or @import of an Apple framework (<UIKit/UIKit.h>, <objc/runtime.h>) goes to the Apple SDKs island Swift uses. A framework header of a pod (<AFNetworking/AFNetworking.h>, <SDWebImage/UIImageView+WebCache.h>), a module (@import Firebase;) and a bare header named like a pod ("Masonry.h") resolve to the pod the nearest Podfile or podspec declares or Podfile.lock pins, matched by name, its module spelling (libPhoneNumber_iOS), without a platform suffix (lottie-ios is Lottie) or by a table of modules named otherwise (GRDB is GRDB.swift's); a header found in a committed Pods/ directory is its pod's, not the project's. A pod line's subspec (Firebase/Analytics) is its pod, :path pods are the project's own directories, and Podfile.lock gives every pod's version and what it depends on; with --online, a pod the lock does not cover is read from the CDN or from a clone of its spec repository in ~/.cocoapods/repos. Cartfile entries are the Carthage island, named by repository as Swift packages are (github.com/Mantle/Mantle) and pinned by Cartfile.resolved; --resolve-depth follows the Cartfile of each dependency checked out into Carthage/Checkouts/. Classes, categories (NSString(Shop)), protocols, methods by selector (Cart.addItem:count:), properties, C functions, NS_ENUMs, typedefs, constants and macros are symbols. The HEADER_SEARCH_PATHS and USER_HEADER_SEARCH_PATHS of an Xcode project, from its project.pbxproj or its .xcconfig files, join the include path of the files below it, C and C++ files too: $(SRCROOT), $(inherited), settings defined beside them and recursive /** entries are understood, and entries outside the repository are dropped. Objective-C is read by a small scanner: the tree-sitter grammar took 19 to 36 ms per file and failed on one file in twenty.

Dart files are read for their import, export, part and part of directives; a conditional import contributes every URI it names (if (dart.library.io) 'io.dart'), since each is compiled on some platform. A relative URI resolves against the file, and package:<name>/<path> to lib/<path> of the package named: the file's own (the nearest pubspec.yaml), a path dependency or override (pubspec_overrides.yaml included), a path package of pubspec.lock, a member of the same pub workspace or a package of the same melos repository. Any other package comes from pub, as pubspec.lock (read from disk when git-ignored; a workspace member's is the root's) pins it or as pubspec.yaml declares it; dart: libraries form a Dart SDK island and Flutter's own packages (flutter, flutter_test, flutter_localizations, anything taken sdk: flutter) a Flutter SDK island. A pubspec's dependencies are imports of what they declare, and a workspace's members imports of their pubspecs. Dart is read by a small scanner rather than the tree-sitter grammar, which on real Flutter code was slow and failed on one file in ten.

Elixir files are read for the modules they name — in alias (including alias Foo.{A, B} and __MODULE__), import, require and use, and in any other reference: a remote call, a struct, a behavior — expanded through the aliases in effect, those that the quote blocks of a used module of the project inject (use MyAppWeb, :controller) and a Phoenix router's scope alias; and for the Erlang modules they call (:ets.new). Erlang files are read for -include, -include_lib, -behaviour, -import and remote calls (mod:fun). A module resolves to the file that defines it — every defmodule in the repository, umbrella applications included, and <module>.erl — or, when the project defines only a prefix of it (generated route helpers), to that prefix's file. Elixir's own modules form an Elixir standard library island and OTP's modules and applications an Erlang/OTP island. Any other module is attributed to a Hex package: the one that defines it under deps/ or _build/ when those are on disk, else the package a curated table or its name prefix names among those mix.exs and rebar.config declare and mix.lock and rebar.lock lock (Phoenix.LiveView to phoenix_live_view, cowboy_req to cowboy). The locks pin, and mix.lock's requirements give the edges between packages. Manifest dependencies and an .app.src's applications are imports of what they name. Both languages are read by small lexers: the tree-sitter grammars were slower and lost Erlang files to macros in patterns.

R scripts, packages and the R chunks of R Markdown and Quarto documents are read for library(), require(), requireNamespace(), loadNamespace(), pacman::p_load(), box::use(), every pkg:: qualifier and roxygen @import tags, and for the files source() (with here::here() or file.path()), box::use(./module), targets::tar_source() and a knitr child pull in. A package name resolves to the importing package's own R/ directory, another package of the repository, R's base packages (and a recommended package such as MASS or survival that the project neither declares nor locks) as a hidden island, and otherwise to CRAN or Bioconductor as renv.lock or packrat.lock pins it or the nearest DESCRIPTION declares it. Files of one package import nothing from each other, so a call to a function the package defines links the calling file to the defining one. DESCRIPTION and NAMESPACE are imports of what they list. R is read by a small lexer; the tree-sitter grammar parsed well but was about nine times slower.

Haskell modules, literate modules (bird tracks and \begin{code}), boot and hsc2hs files are read for their import declarations, PackageImports and {-# SOURCE #-} included. A module resolves to the file whose header declares it - in the importer's own package, then a package of its project or one it depends on - and otherwise to the package that provides it: base, ghc-prim, template-haskell, ghc and GHC's other own libraries as a hidden island, anything else to Hackage, found by a curated module table and by matching the module's name against the packages build-depends declares (Network.HTTP.Client is http-client). cabal's build plan (dist-newstyle/cache/plan.json), cabal.project.freeze, exact cabal.project constraints, stack.yaml.lock and extra-deps pin; a range floats, and a package a Stackage snapshot fixes shows the snapshot's name, as its version is not known offline. build-depends, hpack dependencies, cabal.project and stack.yaml packages and extra-deps are imports of what they name (a packages: glob such as libs/*/ of every package it matches), and the local project files cabal.project imports (import:) add their packages and constraints. Haskell is read by a small lexer; the tree-sitter grammar was several times slower and lost 18% of the files measured to CPP and extensions.

Lua, LuaJIT, Luau (Roblox's, in .luau and .lua files) and Teal files are read for their require calls, dofile and loadfile. A module resolves to the file a rockspec's build.modules maps it to, else to ?.lua or ?/init.lua under the requiring file's directory, each directory above it and their lua/ (a Neovim plugin's), src/ and lib/, then .luarc.json's library; the standard library and LuaJIT's modules are a hidden island, and so are the modules host programs provide (Neovim's vim.*, LÖVE's love.*, OpenResty's ngx.* and bundled resty.* libraries, Lune's @lune/*). Anything else is a rock, found among what the rockspecs declare and luarocks.lock pins by a curated table (lfs is luafilesystem, ssl luasec) and the usual spellings of its name. Roblox's require(script.Parent.X) and game:GetService("ReplicatedStorage").Shared.X are placed as Rojo builds the game from its project files, Luau's require("./x"), "@self/x" and .luaurc aliases by path, and a path through a Packages folder to the Wally package wally.toml names so. --resolve-depth follows wally.lock and the rockspecs LuaRocks keeps for the rocks it installed into lua_modules/ or .luarocks/, and with --online the Wally registry wally.toml names (a GitHub repository, read file by file). Rockspec dependencies and wally.toml entries are imports of what they name, and the top-level functions, methods, module tables, exported fields and Luau and Teal types are the files' symbols. Lua is read by a small lexer; the tree-sitter grammar took 3 to 5 ms per file and failed on Roblox's .lua files, which are Luau.

Perl files (.pl unless it reads as Prolog, .pm, .t, .psgi, Makefile.PL and scripts whose #! line runs perl) are read for use, no, require and do, the classes use parent, use base, Mojo::Base, Moose's extends and with and Corinna's :isa name, and Test::More's use_ok. A module resolves to Foo/Bar.pm under the file's use lib directories (literal, or computed from FindBin, __FILE__, Mojo::File's curfile or Path::Tiny), its distribution's lib/ and t/lib, the lib/ of each directory above it and the repository's; the modules perl ships are a hidden island, unless a manifest requires one with a version or Carton's cpanfile.snapshot installs it (a dual-life module such as List::Util). Anything else is a CPAN distribution, named as MetaCPAN names it (libwww-perl for LWP::UserAgent): the one cpanfile.snapshot says provides the module, else the one the manifests (cpanfile, META.json, META.yml, Makefile.PL, Build.PL, dist.ini) require the module or a namespace above it from (Plack::Request is Plack's), else a distribution named by a curated table or after the module, unresolved. The manifests' requirements are imports, and packages, subs, constants and Moose attributes are the files' symbols. Perl is read by a small lexer that knows POD, here-documents and quote-like operators; the tree-sitter grammar took 5 to 23 ms per file and failed on 3 to 6% of the files measured.

OCaml files (.ml, .mli, ocamllex's .mll and Menhir's .mly) have no imports: they name modules, and dune decides what a name means. Every module path a file names (Foo.bar, open Foo, include Foo, module M = Foo, a functor's argument; not a constructor such as Some x) resolves to the file of the module in the same dune library, executable or test (its directory, the tree include_subdirs adds, the modules its modules field selects), to what a module of the project the file opens declares (open Import), to a library of the repository the component uses (Shop.Cart of a wrapped library, any module of an unwrapped one), to OCaml's standard library (a hidden island; List after open Core is Core's), or to the opam package of a library the component's libraries names (lwt.unix is lwt, Lwt_io is lwt's). dune's libraries and ppx rewriters, dune-project's and opam files' dependencies and pin-depends are imports of what they name, pinned by *.opam.locked, dune's dune.lock/, {= "1.2"} or a pinned commit. The top-level lets, types, modules, exceptions, classes and vals, and dune's libraries and executables, are the symbols. OCaml is read by a small lexer; the tree-sitter grammar took 3.7 to 7 ms per file and failed on 2 to 11% of the files measured.

Julia files (.jl) name modules with using and import and pull files in with include. A file is placed in a module through the include graph, so a relative using .Sub or ..Parent resolves to the file defining that module; using Shop from the package's own tests, docs or extensions is its src/Shop.jl (a submodule, Shop.Cart, the file defining it), and a dependency the nearest Project.toml declares is a package of the repository when its UUID, a [sources] path or the manifest's path says so, a standard library (a hidden island, unless the manifest installed an upgradable one such as Statistics from a registry), or a Julia package pinned by Manifest.toml — found on disk when not committed, a versioned Manifest-v1.11.toml first — or floating on its [compat] entry, where a bare 1.2 is a caret range and only =1.2.3 pins. include("x.jl") and Revise's includet are edges to the file, joinpath(@__DIR__, ...) included. Project.toml's dependencies, extensions and workspace projects and Manifest.toml's entries are imports of what they name. Modules, functions (each once, whatever its methods), macros, types, constants and enums at the top level of modules are the symbols. Julia is read by a small lexer; the tree-sitter grammar took 21 to 314 ms per file and failed on 6 to 28% of the files measured.

Zig files (.zig) name files, the standard library and modules with @import. A path ("cli/args.zig") and @embedFile are relative to the importing file, std and builtin are the hidden standard library, and root is the root source file of the compilation that reaches the file. A module name resolves through the package's build code, read without running it: b.addModule and b.createModule with a root_source_file, .imports lists, addImport and addAnonymousImport (and Zig 0.11's step.addModule), followed through variables, if (...) |dep| captures, struct fields and function returns, to a project file, a build.zig.zon dependency's .module() or a generated options module, which is dropped; unwired, it is the dependency of that name. b.path() in build code is an edge to the file, b.dependency() to the package. @cInclude inside @cImport resolves as a C #include does. build.zig.zon's dependencies are imports: a .path is a directory of the repository, a .url a package named after its repository (github.com/ziglibs/known-folders, from an archive or a git+https URL) with the URL's commit or tag as its version, pinned by its .hash. Functions, tests, containers (with their members as Type.member), constants and variables at the top level are the symbols. Zig is read by a small lexer; the tree-sitter grammar took 8 to 14 ms per file and failed on up to 7% of the files measured.

Clojure, ClojureScript and babashka files (.clj, .cljs, .cljc, .bb, and scripts whose #! line runs bb) name namespaces in the ns form's :require, :use and :require-macros and in top-level require calls, prefix lists included, and every branch of a reader conditional is read; #_ discards a form and :as-alias loads nothing. A namespace is the project file at its path (shop.db-util is shop/db_util.clj, .cljs or .cljc by the importer's platform) under the source paths of the manifests above the file - deps.edn :paths and aliases' :extra-paths, project.clj :source-paths and :test-paths, shadow-cljs.edn :source-paths, bb.edn :paths - and of the projects their :local/root dependencies name, else any file whose ns form declares it. clojure.core, clojure.string and the rest of Clojure's own namespaces, ClojureScript's cljs.* and the Closure Library (goog) are the hidden Clojure standard library, as are the libraries built into babashka for its scripts; anything else is the Maven artifact a manifest declares for it, found by a table of popular libraries (ring.util.* is ring/ring-core, honey.sql is com.github.seancorfield/honeysql) and by naming habits (next.jdbc, cheshire.core, taoensso.timbre, reitit.ring from metosin/reitit-ring), else an unresolved one. :import names JDK classes (the Java standard library), records and types of the project's namespaces, Java files of the project, and classes of declared artifacts, matched as Java imports are (see above: com.google.common is com.google.guava/guava, com.fasterxml.jackson.annotation is jackson-annotations beside a declared jackson-databind, at its version), else by their coordinates; a class of a jar only a dependency brings is left out rather than guessed; a ClojureScript string require (["react" :as react]) is an npm package from the package.json beside the build. The manifests' dependencies - deps.edn and bb.edn :deps and aliases, project.clj :dependencies, profiles and :plugins, shadow-cljs.edn and build.boot :dependencies - are imports: Maven artifacts named group:artifact ([ring "1.9.0"] is ring:ring), git dependencies by their lib name with their repository as origin, and :local/root projects. Namespaces, top-level def, defn, defmacro, defmulti and defmethod, protocols (with their methods), records, types and deftest are the symbols. Clojure is read by a small reader; the tree-sitter grammar took 3.7 to 6.6 ms per file (28 s for metabase).

Bazel's BUILD, .bzl, MODULE.bazel and WORKSPACE files are Starlark. A load() and the labels of a target's label attributes (srcs, hdrs, data, deps, runtime_deps, exports, proto and any attribute ending in deps) name files of the repository - "util.cc", ":util", "//lib:helpers", "@//lib" - which resolve to the file, else to the BUILD file of the package (a rule, or a file a rule generates); glob() is expanded against the package's files, not descending into subpackages. A label of another repository (@repo//pkg:x) resolves to what declares it: a bazel_dep in MODULE.bazel - a module of the Bazel Central Registry at the version MODULE.bazel.lock selected or the one declared, or what a single_version_override, git_override, archive_override or local_path_override puts in its place - a WORKSPACE (or .bzl macro) http_archive named by its URL and pinned by its sha256, a git_repository, a local_repository directory, or the hub repository of a module extension: @maven//:com_google_guava_guava and artifact() are the Maven artifact com.google.guava:guava at the version maven_install.json pins, @pypi//requests and requirement("requests") the PyPI distribution of the requirements lock, @com_github_pkg_errors//:errors the Go module go_deps read from go.mod, //:node_modules/lodash (rules_js) the npm package pnpm-lock.yaml resolved, @crates//:serde the crate of Cargo.lock - the same packages the other plugins name, so they meet on one island. Repositories Bazel provides (@bazel_tools, @local_config_cc) are hidden; one nothing declares is an unresolved module. MODULE.bazel's and WORKSPACE's declarations are imports themselves. Targets (//pkg:name, kind = the rule or macro), .bzl functions, rules, providers and globals, and the module's name are the symbols. Macros are not expanded. Starlark is read by a small scanner; the tree-sitter grammar took 2.2 to 7.2 ms per file (4.6 s for envoy's 1981 files).

Terraform and OpenTofu configurations, variable files, lock files and Terragrunt configurations are read by a small HCL scanner: the tree-sitter grammar parsed every file measured correctly but was about twenty times slower. See Infrastructure as code.

Jsonnet and CUE are read by small lexers: the tree-sitter grammars parsed all but about 1 % (Jsonnet) and 2 % (CUE) of the files measured correctly, at 2 to 4 ms per file, while imports and top-level declarations need only tokens. See Jsonnet and CUE.

Dhall, Puppet and Rego are read by small scanners too (Dhall by the reader spago's files already used): the tree-sitter grammars took 2.5 (Dhall), 9 (Puppet) and 4.7 (Rego) ms per file and left errors in 104 of 154 Puppet files and 282 of 431 Rego files measured (the Rego grammar predates if and contains), while the scanners take 0.02 to 0.1 ms per file. See Dhall, Puppet and Rego.

GLSL, HLSL and WGSL are read by small scanners as well: the tree-sitter grammars left errors in 31 of 345 GLSL, 154 of 377 HLSL and 309 of 328 WGSL and WESL files measured (naga_oil's and WESL's imports are no WGSL) at 2 to 9 ms per file, while the scanners take under 0.2 ms. CUDA, OpenCL and Metal go through the C/C++ scanner; the CUDA grammar took 41 ms per file. See Shaders and GPU code.

Nix expressions are read by a small lexer and parser: the tree-sitter grammar parsed almost every file measured correctly but took 1.9 ms per file, and 12 s for nixpkgs' python-packages.nix alone. See Nix.

Gleam modules are read by a small lexer: the tree-sitter grammar parsed 207 of 209 files measured correctly but took 4.9 ms per file, and everything needed is token-level. See Gleam.

Elm modules are read by a small lexer too: the tree-sitter grammar parsed 411 of 412 files measured correctly but took 4 to 7 ms per file, and Elm's layout rule puts every top-level declaration in column 0. See Elm.

PureScript modules are read by a small lexer as well: the tree-sitter grammar took 86 ms per file and parsed 87 of the 380 files measured with errors. spago.dhall and packages.dhall are read by a small Dhall evaluator. See PureScript.

Crystal is read by a small lexer too: the tree-sitter grammar took 6 to 25 ms per file and parsed 259 of the 715 files measured with errors. See Crystal.

F# is read by a small lexer and an indentation-based scanner: the tree-sitter grammar took 55 to 816 ms per file (17.9 s for one), parsed 25 of the 97 files measured with errors and did not finish FSharp.Core's sources within 15 minutes. See F# and Paket.

D is read by a small lexer too: the tree-sitter grammar took 27 to 71 ms per file (1.1 s for one) and parsed 27 of the 563 files measured with errors. dub.sdl is read by a small SDLang reader. See D and dub.

Fortran is read by a small statement reader, in free and fixed form: the tree-sitter grammar reads free form only (2099 of LAPACK's 2141 fixed-form files parsed with errors) and took 47 ms per file on json-fortran (2.1 s for one). See Fortran and fpm.

Haxe is read by a small lexer and declaration scanner: the tree-sitter grammar parsed 905 of the 983 files of tink_core, HaxeFlixel and Heaps measured with errors, at 9 to 13 ms per file. See Haxe and haxelib.

Ada and GNAT project files are read by a small lexer and block scanner: the tree-sitter grammar parses Ada well, but at 5 to 8 ms per file it took forty times as long as the scanner, and the resolver reads every source's unit besides. See Ada, GPR and Alire.

Racket, Scribble documents and info.rkt are read by a small reader: the tree-sitter grammar parses Racket well but took 3.6 to 5.8 ms per file, and it does not read Scribble's @-forms. See Racket and raco.

Common Lisp, .asd files and qlfile.lock are read by a small reader: the tree-sitter grammar took 30 ms per file and parsed a fifth of the files measured with errors. See Common Lisp and Quicklisp.

Solidity is read by a small scanner: the tree-sitter grammar parsed every file measured correctly but took 7 to 28 ms per file on average. See Solidity, Foundry and Hardhat.

Nim, NimScript and .nimble files are read by a small lexer and an indentation-based scanner: the vendored tree-sitter Nim grammar is licensed under the MPL-2.0 and is left out of this build. See Nim and nimble.

Protocol Buffers definitions are read by a small scanner: the tree-sitter grammar took 2.4 to 3 ms per file and failed on every file using editions. See Interface definitions.

Shell scripts are read by a small scanner: the tree-sitter bash grammar took 2.7 to 3.6 ms per file, about twenty times longer, and failed on two thirds of the zsh files measured. See Shell scripts.

CMake files are read by a small scanner: the tree-sitter cmake grammar parsed every file measured correctly but took 6.4 ms per file on average.

C and C++ definitions are read by a scanner too: the tree-sitter C and C++ grammars parsed more than half of the files of the projects measured with errors, and grpc's generated protobuf tables ran each into the parse bound, 90 s for one analysis (3.5 s with the scanner).

Files in other languages appear on the map without dependency edges. Parsing uses a pure-Go tree-sitter runtime for JavaScript/TypeScript, Python, Rust, Java, Kotlin, Scala, PHP and Ruby; Go uses the standard library's own parser, CI, Compose, Buf and shards files a YAML parser, and C, C++, C#, PowerShell, Markdown, Dart, Elixir, Erlang, R, Haskell, HCL, Protocol Buffers, shell scripts, CMake files, Swift, Objective-C, CocoaPods and Carthage manifests, Lua, Luau, Teal and LuaRocks files, Perl and its CPAN manifests, OCaml, dune and opam files, Julia, Zig and build.zig.zon, Clojure and its EDN manifests, Bazel's Starlark files, Nix expressions, Gleam, Elm and PureScript modules, spago's Dhall files, Crystal, F# and Paket's files, D and dub.sdl, Fortran, Haxe and its build files, Ada and GNAT project files, Racket, Scribble and info.rkt, Common Lisp and its Qlot and ocicl files, Solidity and Foundry's remappings.txt and .gitmodules, Nim, NimScript, .nimble files and nim.cfg, Jsonnet, CUE, Dhall, Puppet, Rego, GLSL, HLSL, WGSL, Dockerfiles, the markup of Vue, Svelte and Astro components, R Markdown chunks and C preprocessor directives small built-in scanners — so the binary continues to cross-compile without a C toolchain.

Security

The server binds to loopback by default and prints a URL containing a random token, which the browser exchanges for a cookie. Requests lacking the token, requests carrying a foreign Host header while bound to loopback — that is, DNS rebinding — and requests for files outside the analyzed project are all rejected. Nothing may frame the map: X-Frame-Options: DENY and frame-ancestors 'none'. A file's raw bytes, which the side panel previews pictures, clips and recordings from, are served only under a known image, video or audio type or as application/octet-stream, and always with default-src 'none'; sandbox, so nothing in a repository can run as the map.

A repository is read only inside itself. The scan lists no symbolic link, and what the analysis reads besides the files it lists - a lock file git ignores, the dependencies a package manager installed into the project, a report or a .depphunter.yaml the repository names - is opened through the repository's root: a symbolic link that stays inside the repository is followed, as pnpm's node_modules/.pnpm links are, but one that leads out of it, or a path that climbs out with .., is not, so a committed link cannot get a file elsewhere on this machine parsed into the map or sent to a package index. Each such file is measured once it is open, so a link cannot pass off a large file as a small one. This machine's own configuration, caches and installed packages outside the repository are read where the package managers keep them.

The side panel shows a file's source; a binary file's content stays hidden behind a button that shows its first 64 KB as a hex dump, since its bytes are rarely worth reading and there can be a great many of them. For such a file the corner's open button becomes Hex editor ↗: in the VS Code extension it opens the file in Microsoft's Hex Editor, which edits the bytes and their text side by side, and offers to install it if it is missing. A launcher on the command line cannot ask for one, so elsewhere the file opens as it is, and the status line says how to reopen it in the Hex Editor. depphunter itself never writes to the repository.

Inside an editor

That last provision is also what prevents an editor from displaying the map in its own built-in browser, which is a frame like any other. --embed <origin> permits the origins it names and no others; it is what a wrapper such as an editor extension passes when it starts the server, naming every frame above the page, since frame-ancestors is evaluated against the entire chain rather than the immediate parent (see How the framing works). Three consequences follow, and nothing else changes:

  • frame-ancestors names those origins rather than 'none', and X-Frame-Options is not sent, since it provides no means of naming an origin that browsers still honor.
  • The token remains in the address rather than being exchanged for a cookie. A cookie set by the map is a third-party cookie within another origin's frame, and browsers do not return those; the page reads the token from its own URL and supplies it on every call, in a header, or in the query string for the event stream, which cannot set headers. Referrer-Policy: no-referrer keeps it out of any outgoing request.
  • The interface's static files — the page, app.js and its imports, styles and models — are served without the token, since a frame cannot attach one to a <script src>. They are identical in every release and disclose nothing about the project. Everything under /api, and the entry address /, still requires it.

The option is deliberately a flag and nothing else: no configuration file and no environment variable can enable it, so a repository cannot arrange to be framed by a page of its choosing. Each origin is validated before it reaches the header, so nothing passed on the command line can terminate the directive early or begin another.

Contributing

Building and testing, the structure of the code, and working on the VS Code extension are documented in CONTRIBUTING.md.

License

BSD 3-Clause © 2026 Dawid Ciepiela. The embedded three.js (MIT), highlight.js (BSD 3-Clause) and fzf-for-js (BSD 3-Clause), and the 3D models built from webxr-input-profiles (MIT) and a low-poly nature pack (CC0), retain their own licenses; see web/static/vendor.