Pick your kit, install it anywhere, prove what you used.
A security tool manager that isn't tied to one distro or one package manager.
Why · Install · Everyday use · Browser · Catalog · Config · Security · Contributing
The tools worth having no longer ship from one place. nuclei, subfinder and
httpx come from the Go toolchain; impacket and prowler from pipx;
hayabusa and velociraptor from GitHub releases; plenty more from apt or
Homebrew. Loadout describes each tool once and installs it with whichever
backend your machine actually has.
loadout install nuclei # apt on Kali, brew on macOS, go anywhere else
loadout bundle create -l dfir-responder -o kit.tar # then install it with no network
loadout sync # converge this box to the team's loadout.yaml
loadout report --since 30d # what you used, and how each download was checkedapt |
Loadout | |
|---|---|---|
| Works on Kali, Ubuntu, Arch, macOS, WSL | partly | yes |
| Installs Go / Rust / pipx / release-binary tools | no | yes |
| Verifies downloaded binaries against published checksums | n/a | yes, by default — and shows you it did |
| "Which tools exist for lateral movement?" | no | loadout phase lateral-movement |
| "What should I use instead of dirbuster?" | no | loadout alt dirbuster |
| Install onto a machine with no internet | no | loadout bundle |
| Reproduce this exact toolset on another box | no | loadout sync |
| Prove which tool versions an engagement used | no | loadout report |
Nothing is published to PyPI yet, so Loadout installs from a checkout. The same five lines work on Kali, Debian, Ubuntu, Arch, macOS and WSL.
git clone https://github.com/MushroomCyber/Loadout.git
cd Loadout
python3 -m venv .venv
source .venv/bin/activate
pip install -e '.[tui]'loadout doctor # is this machine ready?
loadout # the interactive browserThe first launch opens the browser. Type to filter, arrow keys to move, enter
to act on the highlighted tool. Everything is also clickable — see
Interactive browser.
Prefer pipx?
pipx install --editable .from the same checkout. (pipx install loadoutwould fetch a published package; there isn't one yet.) Add thedevextra —pip install -e '.[dev,tui]'— only if you intend to run the test suite, since it pulls pytest, ruff and mypy.
Two different things get upgraded, and they are separate commands on purpose: Loadout itself comes from this repository, and the tools it installed for you come from their own package managers.
To upgrade Loadout, from Loadout:
loadout self-update --check # what would change, and where it would come from
loadout self-update # fast-forward this checkoutor press ctrl+u in the interactive browser, which does the same thing behind a
screen that names the remote before it offers the button.
It is deliberately narrow. It refuses rather than guesses:
| Situation | What happens |
|---|---|
| Uncommitted changes in the checkout | refused — this may be your working copy |
| Local commits the remote doesn't have | refused — resolve it with git, not with a button |
| Anything but a fast-forward | refused — it can never rewrite history you have |
| A dependency change | reported, never auto-installed |
It runs git and nothing else: no pip, no hooks, no privilege escalation. It
also merges the exact commit it showed you, not whatever the branch moved to
while you were reading. Restart loadout afterwards — the running process
already imported the old code.
The equivalent by hand, which self-update is a wrapper around:
cd Loadout
git pull
source .venv/bin/activate
pip install -e '.[dev,tui]' # only needed when dependencies changed
loadout --version
loadout catalog info # confirm the new catalog is the one in useWith a pipx-managed checkout, pipx install --editable . --force instead of the
pip install line.
git pull brings a rebuilt catalog with it, so there is nothing to compile.
Run loadout catalog build only if you edited catalog/ yourself.
Your data is untouched by an upgrade: loadouts, history, stars and provenance
live in $XDG_STATE_HOME and $XDG_CONFIG_HOME, never inside the package.
If
loadout catalog infostill shows the old tool count, you have a refreshed catalog in$XDG_DATA_HOME/loadout/catalog.dbfrom a previousloadout catalog update, and it deliberately wins over the one shipped with the release — otherwise an upgrade would throw away a refresh. Re-runloadout catalog updateto rebuild it from this machine's package metadata, or delete that file to fall back to the catalog you just pulled.
To upgrade the tools themselves:
loadout update # refresh package lists, report what is upgradable
loadout upgrade --dry-run # show exactly what would change
loadout upgrade # apply it
loadout hold burpsuite # pin a version an engagement depends onIf an upgrade leaves the machine in an odd state, loadout doctor names the
problem instead of leaving you to guess.
loadout # interactive browser, filter as you type
loadout search fuzz # full-text search across names and summaries
loadout show ffuf # everything known, including how to install it here
loadout list --category web # browse a category
loadout phase lateral-movement # browse by engagement phase
loadout alt dirbuster # what people use instead
loadout providers # which installers work on this machineloadout install ffuf nuclei subfinder
loadout install nuclei --dry-run # show the plan, change nothing
loadout install nuclei --provider go # force a backend
loadout install nuclei --prefer brew # nudge the resolver
loadout remove nikto --yes
loadout run gowitness scan file -f hosts # run it, in a container if not installedLoadout picks a provider per tool, per machine. --dry-run prints exactly what
would run before anything happens:
Plan (install):
nuclei via go
$ go install github.com/projectdiscovery/nuclei/v3/cmd/nuclei@latest
ffuf via apt
$ apt-get install -y -o Dpkg::Options::=--force-confold -- ffuf
A loadout is the kit for a job. Bundled ones cover common roles; you can write your own or capture what you already have.
loadout loadout list
loadout loadout show ad-operator
loadout loadout apply dfir-responder
loadout loadout save my-kit # snapshot this machineCommit a loadout.yaml to the engagement repo and everyone gets the same box:
slug: acme-webapp-2026
name: ACME web app test
tools: [ffuf, nuclei, sqlmap, burpsuite, httpx, katana]loadout sync # install what's missing
loadout sync --prune # and remove what isn't declared
loadout loadout diff acme-webapp-2026A manifest lists tool ids and no versions, so applying it in March and again in
September builds two different boxes. loadout lock records what those ids
actually resolved to — provider, version, and how the download was checked —
into a loadout.lock you commit beside the manifest.
loadout lock # write it from this machine
loadout lock --check # does this box still match? non-zero if not
loadout sync # reports drift against the lock when it existsDrift is reported per tool, in three kinds that are kept apart:
| Kind | Means |
|---|---|
| version differs | Names both sides, so you can see which way it moved |
| different provider | Same version number, different build. nmap 7.95 from apt and nmap 7.95 from a release archive are not the same binary |
| no version to compare | The provider could not tell us. Reported, never passed silently |
The lock records and compares. It does not force a version at install time.
Only go can express a version pin today, so a lock that claimed to pin
everything would be pinning about a fifth of it. Saying exactly where a box
has diverged is a smaller claim, and one this can actually support.
| Loadout | For |
|---|---|
pentester-web |
Web application testing |
recon-modern |
The Go-toolchain recon pipeline, no apt needed |
ad-operator |
Active Directory assessment and lateral movement |
bug-bounty |
Public programme recon and web |
dfir-responder |
Host and log forensics |
detection-engineer |
Building and validating detections |
cloud-auditor |
Cloud and container posture |
ai-redteam |
LLM applications, agents, and model-file scanning |
forensics-starter, osint-minimal, ctf-basics |
Starter kits |
Incident response happens on isolated segments. Client sites block egress. Classified work has no route out at all. Build the kit where there is network, carry it in, install it where there is none.
# on a connected machine
loadout bundle create -l dfir-responder -o kit.tar
loadout bundle inspect kit.tar # what is in it, and what was left out
# on the isolated one
loadout bundle verify kit.tar # intact? built for this architecture?
loadout bundle install kit.tarA bundle is a tar holding a manifest.json and the artifacts themselves — apt
packages with their full dependency closure, verified GitHub release
binaries, Python wheels, and RubyGems. Nothing about installing it touches the
network.
Every route carries its whole dependency closure, which is not what the obvious
command gives you. gem fetch zsteg downloads one gem; the install needs
seven, so a bundle built that way fails on the isolated machine looking for
prime. Loadout resolves the closure instead and carries all seven.
Python wheels are tied to an interpreter version — pip download on a 3.13 box
pulls cp313 wheels — so the bundle records the Python it was built for and
refuses on a mismatch and names both versions. The alternative is an
obscure failure somewhere with no network to fix it from.
The bundle is the one thing here that arrives from outside, onto a machine picked precisely because it is isolated, so it is treated as untrusted input:
- every file is checksummed into the manifest when built and re-checksummed before use — otherwise a bundle would be a clean way around all the checksum and signature verification everywhere else;
- member paths are validated before extraction, so no
.., absolute path, symlink or device node can write outside the target; - the build platform is recorded, because a bundle of amd64 debs is not a kit on an arm64 box and mid-engagement is the wrong time to find out.
What cannot travel. go and cargo need a build toolchain on the far
side. npm is left out for a different reason: the offline path has never been
tested end to end, and shipping it untested would mean it failing on the
isolated machine instead of at build time.
Either way you are told. Each excluded tool is listed with its reason when the bundle is built. A bundle that quietly held less than it claimed would be found out on the isolated machine, which is the worst possible place. Where a tool has both a bundleable and a non-bundleable route, the bundleable one wins even if the catalog ranks it lower.
An install that reported success is not the same as a tool that runs. A Go
binary can land off PATH, an apt package can install while its interpreter
does not, a release archive can hold the wrong architecture — all three report
"installed" and all three fail on site.
loadout verify # everything installed
loadout verify nmap ffuf # just these
loadout verify --quiet # only what is broken✓ ffuf ffuf version v2.1.0
✓ nmap Nmap version 7.99 ( https://nmap.org )
✗ nuclei nuclei: not found on PATH
2 verified · 1 failed
It exits non-zero if anything failed, so it works as the last line of a pre-engagement script. The four outcomes are kept separate, because they are not equally strong claims:
| Outcome | Means |
|---|---|
| verified | The catalog's verify: command ran and exited 0 |
| on PATH | The binary was found, but never run |
| failed | It did not work |
| not checkable | The catalog knows neither a command nor a binary name |
Wordlists and payload collections have no command to run, so they are checked
differently. Entries marked kind: content — SecLists, the Kali wordlists,
PayloadsAllTheThings — declare paths: instead of binaries:, and verify
checks those paths exist. Before that distinction existed they were hunted for
a binary they were never going to have, and a working 1.8 GB install reported
as failed. loadout run on one of them prints where its files are instead of
trying to execute a directory.
Pentest reports and DFIR chain-of-custody both need "which tools, which versions, when". Loadout records that as it works, so the report is evidence rather than recollection.
loadout report --since 30d --format markdown -o tooling.md
loadout audit # unmaintained, superseded or unverified tooling
loadout history --tool nucleiThe report says how each tool's download was checked. Anything that arrived
unchecked gets its own section, along with the --allow-unverified that
permitted it. Three outcomes are kept apart:
- a check that passed, named by method (
checksum,gpg); n/a, where there was no check of ours to run — apt verifies its own package signatures;- a check that ran with nothing to verify against.
Only the third is an unverified install, and it is the one a challenged finding asks about.
All of it is read out of the install history, not current state, so it survives a reinstall and still answers for a tool you have since removed.
loadout export --format script > install.sh
loadout export --format docker > Dockerfile
loadout export --format ansible > playbook.yml
loadout export --format loadout > loadout.yamlloadout completions bash > ~/.local/share/bash-completion/completions/loadout
loadout completions zsh > ~/.zfunc/_loadout # ~/.zfunc must be in $fpath
loadout completions fish > ~/.config/fish/completions/loadout.fishThese are generated from the parser that actually runs, so they cover every subcommand and flag and cannot fall out of step with them. They print to stdout instead of installing themselves, because where the files belong differs by shell and by distro — and a tool that edits your shell configuration uninvited is one you stop trusting.
Tool ids are left out on purpose. There are 842 of them, they change with every catalog update, and a completion script carrying a stale copy would confidently offer tools that no longer exist.
loadout list --installed --json | jq -r '.[].id'
loadout install nuclei --dry-run --json | jq '.actions[].steps'
loadout doctor --json | jq '.[] | select(.severity != "ok")'
loadout verify --json | jq -r '.[] | select(.status == "failed") | .tool'loadout with no arguments. The cursor starts in the filter box and the list
narrows as you type.
The screenshot at the top of this page is exactly what you get. The banner drops to a single line on terminals under 96x30, so a small window spends its rows on tools instead of on the program's own name.
The whole catalog is browsable with an empty filter — starred first, then installed, then alphabetical. Typing a query switches to relevance ranking and shows the top 500 matches. The hint line says so outright, so the rest are clearly still there.
The detail pane reports what state already knew and never showed: how a tool
was verified, when it was installed, and when it was last run through
loadout run. "last run 6mo ago" is what tells you a kit needs pruning.
An install that fails because upstream published nothing to check the download
against offers an Install unverified button, which does from the browser
what --allow-unverified does from the CLI. It appears only for that one
failure. A checksum mismatch never offers it: those bytes are not the ones
that were published, and no button should make them installable. The waiver lasts
exactly one attempt, and the install it produces is recorded as unverified, so
loadout report names it later.
| Key | Action |
|---|---|
| type | Filter immediately |
↑ ↓ |
Move |
space |
Mark a tool |
enter |
Install / remove the highlighted tool |
ctrl+a |
Install everything marked |
ctrl+s |
Star |
ctrl+p |
Command palette — fuzzy-find and apply a loadout |
ctrl+u |
Update Loadout itself (see Upgrading Loadout) |
f5 |
Re-read what is installed |
esc |
Clear filter, then clear marks, then quit |
Every control calls the same action its key binding does, so nothing behaves differently depending on how you reached it.
| Control | Where | Does |
|---|---|---|
| Install / Remove | Detail pane | Acts on the selected tool; the label tracks its state |
| ★ Star | Detail pane | Toggles the star |
| Run | Detail pane | Only shown when the tool has a known binary or a container image |
| Alternatives | Detail pane | Only shown when the catalog lists any |
| Install n marked / Clear | Batch bar | Appears once something is marked, and counts it |
| Provider toggles | Under the filter box | Narrow the list to one or more backends |
| Category chips | Left sidebar | Filter by category |
| Retry / Close | Install modal | A failed install offers a next step, not just red text |
Two of those carry state you can read at a glance:
- Provider toggles are only offered for backends that are both usable on
this machine and present in the catalog — listing
npmon a box where no entry names it would be a control whose only outcome is an empty table. An active toggle takes the accent colour, bold and reverse. - Category chips show installed-over-total (
wireless 14/48). The active chip takes the accent, bold and reverse -- the only thing a chip's colour communicates is which one you clicked.
The catalog is data, not code: one YAML file per tool under
catalog/, reviewed by pull request and compiled into a SQLite
database with a full-text index.
# catalog/web/ffuf.yaml
id: ffuf
summary: Fast web fuzzer for content and parameter discovery
categories: [web, fuzzing]
tags: [fuzzing, bug-bounty]
phases: [discovery]
binaries: [ffuf]
homepage: https://github.com/ffuf/ffuf
license: Apache-2.0
verify: ffuf -V
alternatives: [feroxbuster, gobuster, dirsearch]
install:
- {provider: apt, package: ffuf, distros: [kali, debian, parrot, ubuntu]}
- {provider: brew, formula: ffuf}
- {provider: go, module: github.com/ffuf/ffuf/v2@latest}
- {provider: github, repo: ffuf/ffuf, checksums: "*checksums*.txt"}loadout catalog validate # check the source tree
loadout catalog build # compile it
loadout catalog update # enrich from local APT metadata
loadout catalog probe-verify # suggest verify: commands by asking the binaries
loadout catalog infoAdding a tool is one file and a pull request. See docs/CATALOG.md.
Coverage is uneven. 651 of 842 entries name the command they install, and
48 go further and carry a verify: command that proves it runs. For each
tool, loadout verify says which of the two it managed, so a weaker check
never reads as a stronger one.
Providers implemented: apt · brew · pipx · go · cargo · gem ·
npm · github (verified release downloads) · docker
Providers with catalog coverage today: apt 774 · brew 33 · pipx 24 ·
github 17 · go 12 · gem 4 · cargo 2 · npm 1. docker works but no
entry uses it yet — adding one is a pull request, not a code change.
| Path | Holds |
|---|---|
$XDG_CONFIG_HOME/loadout/loadouts/*.yaml |
Your loadouts |
$XDG_STATE_HOME/loadout/state.db |
Installs, history, stars, provenance |
$XDG_DATA_HOME/loadout/catalog.db |
The compiled catalog |
./loadout.yaml |
Project manifest for loadout sync |
./loadout.lock |
What those ids resolved to, for loadout lock --check |
| Variable | Effect |
|---|---|
LOADOUT_OFFLINE=1 |
Make no network calls |
LOADOUT_NO_EMOJI=1 |
ASCII glyphs (auto-detected too) |
LOADOUT_THEME |
default, mono, solarized-dark, high-contrast |
LOADOUT_BIN_DIR |
Where release binaries land (default ~/.local/bin) |
GITHUB_TOKEN |
Raises the GitHub API rate limit |
NO_COLOR / FORCE_COLOR |
Respected |
Loadout runs sudo and downloads binaries, so it holds itself to the bar that
implies:
sudois constructed in exactly one function,policy.elevate()— enforced by a test.- Every package name is validated before it reaches an argv, and
--always separates options from names. - Downloaded artifacts are checksummed against the release's own checksum file. No checksum means refusal, not a warning;
--allow-unverifiedis an explicit opt-in. - A check that passed says so. The install screen shows the result in the progress block, above the divider and outside the scrolling log. The CLI prints it without needing
--log-level DEBUG. It is recorded in state, so the detail pane still shows✓ checksum verifiedmonths later. A batch install collapses all of that to a count. A skipped check reads asunverified, never as a pass. - Where upstream signs its releases, the catalog pins the trust anchor and
gpg,minisignorcosignchecks the signature. The signer must be the pinned one. A valid signature by some other key is a failure — a baregpg --verifyexit code would pass it, so Loadout reads gpg's status output and compares the fingerprint. Verification runs against a throwaway keyring, so a catalog entry can never touch your own. - A signature the catalog declares cannot be waived.
--allow-unverifiedis for projects that publish nothing to check against. It is not for a check that ran and failed. - Sigstore is supported in all three shapes projects publish: a
.sigstore.jsonbundle, a detached.sigwith its.pem, and key-based cosign. Keyless pins an OIDC identity instead of a key. Some projects sign from an identity that carries the release tag, so the pin is a pattern, and an unanchored pattern is refused when the catalog is built. cosign matches these unanchored, which meansgithub.com/owner/toolon its own would also accepthttps://evil.example/github.com/owner/tool/.... - Archive extraction refuses absolute paths and
..traversal. - No
shell=Trueanywhere. - Your sudo password never passes through loadout. The browser hands the
terminal back so
sudocan read it from/dev/ttyitself, which is why there is no password box in the app — a nicer-looking one would mean the password transiting this process. A test fails if the credential path ever grows aninput(), a stdin pipe or agetpasscall.
See SECURITY.md for the threat model and how to report an issue.
Adding a tool to the catalog is the easiest useful contribution — one YAML file, no Python. See docs/CATALOG.md for the format and CONTRIBUTING.md for the workflow. There is an issue template if you would rather suggest a tool than write the entry yourself.
pip install -e '.[dev,tui]'
pytest
ruff check .
mypy loadoutOpen issues are the place to look for something to pick up, and the place to propose something that is not there yet.
Pushing a v* tag builds an sdist and a wheel, installs the wheel into a clean
interpreter and runs it, then attaches the distributions and the three shell
completion scripts to a GitHub Release.
git tag v1.0.0 && git push origin v1.0.0The version is written in pyproject.toml and in loadout/__init__.py, and the
tag is a third claim about it. All three must agree or the release fails before
anything is built: artifacts whose contents disagree with their own filename are
discovered by whoever installs them.
PyPI publishing is wired but switched off. It needs Trusted Publishing
configured on PyPI and the PUBLISH_TO_PYPI repository variable set to true —
left inert so the wiring can be reviewed before it is ever able to publish.
MIT. See LICENSE.
