A first swarm in five minutes, a first case after that.
Everything below runs on your own machine, free, under the GNU AGPL. Each agent runs in a microVM of its own, so the agents' image is built once before the first run. The first run costs a few cents on a cloud model or nothing on a local one. Nothing here needs an account with us.
- 1What you needfour things, all of them free
- 2A first swarmone image build, then about five minutes
- 3Your first caseyour own evidence
- 4While it runsask more, answer, continue
The commands up to your first case are the repository's quick start, and they are what the published cases were started with. The ones for a running case are in the usage guide.
Four things, all of them free.
Nothing here is bundled and nothing here is ours to license. Install the four, check each one with the line under it, and the harness runs as it is.
Node 22.19 or newer, which the pinned Pi needs, with jq, python3 and curl, and bash or zsh as the login shell. The scripts run as they are, with no build step.
node --version && jq --version && python3 --versionHerdr gives each agent a terminal pane. Pi is the agent inside it. Each installs with one line.
herdr --version && pi --versionAn API key, a Claude or ChatGPT subscription logged into Pi once with pi /login, or a model served from this machine. No credential goes to an agent: Pi on the host reads its own store, and each VM gets a placeholder the host swaps for the key on the way out.
pi auth check --model provider/idA Mac on Apple silicon, or Linux with KVM and glibc. sqlite3, so a stop can clear a removed VM's secrets from the runtime's database. Docker, once, to build the agents' image and the job images a case's packs need, unless they are pulled from a registry. The microVM runtime comes with npm install.
sqlite3 --version && docker --versionls -l /dev/kvm # on Linux--isolation host runs the same swarm with every agent as a process on this machine, held by the host guards below: sandbox-exec on macOS, which is built in; on Linux, Landlock (kernel 5.13 or newer) and unprivileged user namespaces for the full set. The kickoff refuses a host that cannot boot VMs, says what it lacks, and names host mode as the way on. It never switches to host mode by itself.Each agent in its own microVM, and host mode where a machine cannot run one.
The columns name the mechanism, so you can go and check it. A microVM needs macOS on Apple silicon or Linux with KVM. In host mode the one weak guard is the egress allowlist on macOS: there it is a proxy the panes are pointed at, and a process that ignores it reaches the network. The kickoff line and the report both name it.
sandbox-exec holds the copy read-only on macOS; a read-only bind or a Landlock rule on Linux.noexec in every VM, so a carved binary does not run by mistake.noexec, or the execute right withheld under Landlock, on Linux.Install, build the agents' image, prove the protocol, run two agents, open the console.
Install
# Herdr, with its Pi integration
curl -fsSL https://herdr.dev/install.sh | sh
herdr integration install pi
# Pi
npm install -g --ignore-scripts @earendil-works/pi-coding-agent
# DFIR Swarm, and the microVM runtime with it
git clone https://github.com/halilozturkci/dfirswarm && cd dfirswarm
npm install
# host runs only (--isolation host), without the runtime's native binaries:
# npm ci --omit=optional
Log Pi into a provider once with pi /login. The harness reads Pi's own store on the host, and no key goes to an agent.
Build the agents' image
# the base: amd64 in place of arm64 on an Intel or AMD Linux host
docker build -f images/base.Dockerfile -t dfirswarm-base:dev-arm64 images
docker save dfirswarm-base:dev-arm64 -o /tmp/dfirswarm-base.tar
"$(node --experimental-strip-types scripts/vm.ts msb-path)" load -i /tmp/dfirswarm-base.tar
Every agent's VM boots the base: Pi, a shell, Python and the tool library. The forensic programs are in the job images, one per profile (disk, memory, mobile, network, linux, re, full), built the same way (images/README.md); a run with packs runs its tool work in the images that hold them, and each image lists its programs in a file the agents read. When this machine lacks the base, or the job image that holds all of the run's packs, the kickoff tries to pull it before anything is written, and when it cannot, stops and says how to build it.
Prove the protocol with no model, no key, no network
# two scripted workers: a claim conflict, a blocked write,
# a detected shell write, the sentinel
npm test
# the finish line's certification, the reaper, the egress guard,
# the guards this host can enforce
npm run test:bash
The guard suites skip themselves with a message on a host that cannot enforce a guard, so what passes here is what your machine can hold.
A whole swarm with no key
# a scripted model: real panes, real harness, no key
node tests/mock-provider.mjs --port 8787 \
--script tests/fixtures/mock-swarm-hello.mjs &
scripts/swarm.sh start --isolation host --model mockswarm/scripted-1 --n 2 \
--cap-usd 5 --no-netguard --goal-file prompts/goals/hello.md \
--env PI_CODING_AGENT_DIR=/tmp/piagent
# a model served from this machine, for nothing
scripts/swarm.sh start --isolation host --model ollama/glm-4.7-flash-64k:latest --n 2 \
--cap-tokens 3000000 --wall-clock 12 --local-only \
--goal-file prompts/goals/hello.md
Both were proven as host runs and are shown that way; the repository's VM suite runs the same kind of scripted model with the agent in a VM. The scripted provider is declared in Pi's models.json, and the proof run has the file. A team that bills nothing is capped in tokens, and the kickoff probes the local server before any pane opens. In a VM run the agents reach a local server through the host gateway, on that port alone.
Run two agents on the hello goal
# Herdr must be running: open the app, or `herdr server` in another terminal
scripts/swarm.sh start --model deepseek/deepseek-v4-pro --cap-usd 1 --n 2 \
--goal-file prompts/goals/hello.md
The goal is a markdown file with its own ## Definition of done and ## Checks; a goal without one does not start. The agents run in two microVMs, one each, from the base image. The kickoff says so on its Isolation: line, names the VMs it creates and their size, and prints the run id, the sandbox path and the three commands to watch, check and stop. Each VM's probe goes into its record, and a VM whose probe fails stops the kickoff.
scripts/swarm.sh status <id>
scripts/swarm.sh stop <id>
With --isolation host the same command starts a host run, unisolated: the Isolation: line says so, and one line per guard says what this host could enforce. A two-agent hello run costs a few cents on a cloud model and, as a host run, finishes in a minute or two; VMs add their start-up to that.
Open the console
# http://127.0.0.1:43173, this machine only
scripts/swarm.sh ui
The console binds 127.0.0.1. From another machine, use an SSH tunnel. Watching is open; starting, stopping, reaping and restoring need the token the server prints at startup. The New swarm form starts a run the same way the command line does, with the goal document first and its finish line read back before a model is chosen, and its microVM switch on by default. A run's Jobs tab lists every tool job from the store's journal, with its record, its manifest and its logs, and in the ledger a sensitive entry stays blurred until you click it. The Questions tab asks the running swarm a question and shows who asked each one, the Requests tab what the run is waiting on you for, and the Network tab every lookup in a dynamic run; a paused run shows its notice and its reason with Extend, or Unpause for a pause that is not a cap's, and a stopped one Continue this run. When a run has stopped, its Release tab signs the report, and the Examiners page enrols the people who sign.

The same kickoff, as a form.
Everything the command line takes, the console's New swarm form takes too, and it runs the same swarm.sh start underneath. The screens below are the console on a first case, filled in the order the form asks.


SWARM.md and the agents cannot rewrite it. As you type, the panel beside it reads the finish line back: definition of done present, the checks it found, whether they read work/. A goal without a finish line is refused here as well as at kickoff. An investigation from the library also suggests a team size, a cap and a wall clock, which you apply or leave.
pi auth check answered for each provider right now, and, in a microVM run, whether a VM would take it.


edit, write and claim_file. In a microVM run it is copied into the run by default and mounted read-only into every VM, or shared from where it lies; in a host run auto takes the kernel guard the host has. The clean room names earlier runs whose work the agents may not read.


swarm.sh start line it will run, so what the console does is never different from what the terminal does. Check the start runs every refusal and preflight of that line and writes nothing. Starting needs the token the server printed at startup; watching never does. The run appears in the overview within seconds and opens to its story, board, trace and ledger.Evidence, a goal, and the tools the case needs.
A case differs from the hello goal in three ways. The flags below are the ones every published case ran with; swarm.sh help start explains each.
# the method, fetched a skill at a time
scripts/pack.sh install packs/computer-forensics-base
scripts/pack.sh install packs/windows-forensics
scripts/swarm.sh start \
--models "openai/gpt-5.4=4,deepseek/deepseek-v4-pro=3" --n 7 \
--goal-file library/windows/host-intrusion.md \
--pack computer-forensics-base,windows-forensics \
# hashed, read-only in every VM
--inputs /evidence/case-42 \
--catalog --toolbox dfir --quarantine \
--allow-tool-forging \
--cap-usd 60 --cap-per-agent 12 --wall-clock 90 \
--case-id CASE-42 --examiner "Your name" --label case-42
The agents boot the base, and the packs choose the job images. These two run their jobs in the disk profile, dfirswarm-disk:dev-<arch>, which holds the programs they name; build it as the base was built, and the build fails if a program a pack requires is missing. A job names the image it needs, a pack tool runs in its pack's, and a pack tool the agent's own VM cannot run is run again there as a job by itself.
--inputs DIR hashes every file and mounts the directory read-only into every VM; --inputs-copy gives the run its own read-only copy. work/extracted/ and work/quarantine/ are no-exec in every VM whatever --quarantine says. In a host run --inputs DIR copies the directory into the run and holds it read-only in every pane, --inputs-bind guards the source in place, and on macOS --inputs-image case.dmg attaches a disk image read-only.
Forty-five investigations ship with the repository under library/, one for each kind of case: hosts, servers, memory, logs, captures, malware, cloud tenants, phones, and the general work of triage, indicators and timelines. Each carries the questions with the artefacts that answer them, a definition of done that names the report and the ledger, and checks the agents cannot edit. Load one in the console or name the file here, then change what your case asks. The BelkaCTF goal is a worked example of the same shape.
--toolbox dfir checks for Sleuth Kit, libewf, libbde, Volatility, YARA and the parsers a case needs, and names what is missing before the first agent starts: in a throwaway VM of the job image that holds the run's packs, or on the host in a host run. --catalog takes the census of the evidence and catalogues it with the packs' recipes into catalog/: in a microVM run the recipes run as jobs while the agents work, and what the jobs produce is catalogued in turn. With --allow-tool-forging the agents write the tools the image lacks; swarm.sh tools <id> --save DIR keeps them and --tools-from DIR hands them to the next swarm.
--pack computer-forensics-base,windows-forensics puts an examiner's method in front of the swarm: the agents see a one-line index of every skill and fetch a body only when they reach that artefact family, so a pack the size of a textbook costs a few hundred tokens until it is used. Twelve packs are in the repository under the same licence, for Windows, Linux, macOS, mobile, memory, network, reverse engineering, encrypted containers, cloud, ransomware and triage collections. Install what the case needs with scripts/pack.sh install packs/<name>; the console's Packs tab then says which skills the run read and which it carried and never opened.
Only the model providers are reachable. --allow-host adds a host the case needs for the whole run, host and port only; --allow-install lets the agents install what they need, into each VM's own disk or, in a host run, into the run, inventoried; --no-pypi keeps the index off the allowlist. --network dynamic lets the agents ask for lookups decided by rules instead, and --policy sets what may leave the run. In a host run on macOS the egress guard is advisory, and the record says so.
--no-read DIR keeps a previous run's findings on the same evidence away from the agents: no VM mounts it, and a host run denies it at the kernel, so a second swarm cannot read the back of the book. To hand the second swarm the first one's claims as hypotheses to re-derive or refute, use --ledger-from <run>.
A copied --inputs is checked against its source by content (--no-verify-copy checks name, kind and size only), and the kickoff's Disk: line says whether the volume that holds the runs is encrypted at rest. Keep the runs on a local, encrypted disk that is not synced: a copy of the evidence bound for a synced folder is refused.
--inputs-hashes FILE holds the evidence to the hashes your imager recorded, and a mismatch stops the kickoff. --custody-sign-key signs the custody verdict with your ssh key, --custody-timestamp-url has it timestamped by an RFC 3161 authority, and --time-reference records a reference clock's offset. Every check in the verdict has a status, and swarm.sh custody-verify <id> checks the run against it later, writing nothing. --require-technical-review makes the report's release wait for a technical reviewer's signed review.

swarm.sh report <id> renders the report with its custody section, swarm.sh summary <id> prints what the run cost and what the harness had to do, and swarm.sh package <id> ships the whole run with a SHA-256 manifest. Every published case is that package, pruned of the evidence.Before you hand it over
# once per install: a name, the lab, a competence statement and one key
# (--generate-key for an ssh key with a passphrase, --fido, or --pkcs11-module for a token)
scripts/swarm.sh examiner enroll --name "Your name" --organisation "Your lab" \
--competence "what you examine" --generate-key
scripts/swarm.sh custody-verify <id> # the run, checked against its sealed custody verdict
scripts/swarm.sh review <id> --adopt 41 # or --qualify / --reject / --inconclusive N --note "why"
scripts/swarm.sh review <id> --sign # read what it shows, confirm, then the passphrase, touch or PIN
scripts/swarm.sh releases <id> --verify --allowed-signers signers # every release, against your register
scripts/swarm.sh package <id> --sign # sign the manifest with your ssh key
scripts/swarm.sh verify runs/<id>/package --allowed-signers signers # what the recipient runs
At the stop the machine seals the report's draft as release v0, and until an examiner adopts an answer the report says it is the agents' conclusion. review --sign shows the final report's sha256 before it asks for anything, and the release it writes is v1. A technical reviewer enrolled with --role reviewer signs their own review with review --technical-review, here or from the package on another machine. How a report is signed draws the whole sequence. package --redact takes out what a sensitive entry says and the files it cites, while every chain in the package still verifies, and --with-outputs adds the jobs' sealed outputs. hold <id> keeps a run from being purged or reused; purge <id> --yes deletes a finished run's material and leaves a destruction record. The usage guide has each command.
Ask it more, answer what it asks, and let it go on.
A case rarely ends with the questions it began with. These commands work on a running swarm, a paused one and one that has stopped, and each that changes the run is a line on the operator's record; metrics and replay only read a run that has stopped. The console's Questions, Requests and Network tabs, and its Extend, Unpause and Continue this run buttons, run the same commands. The usage guide has every option.
# at kickoff: the case policy, the network mode, and where you are told
scripts/swarm.sh start … --policy standard --network dynamic \
--more-evidence ask --notify desktop:
# ask the running swarm, as an enrolled analyst, signed with their key
scripts/swarm.sh question <id> add --text "Was the archive opened on this host?" \
--why "the client's second question" --objective O-1 --as ana --sign
scripts/swarm.sh question <id> list
scripts/swarm.sh question <id> clarify-reply Q-9 C-2 "Opened means extracted to disk"
# a question that takes something as happened says so; the premise is tested first
scripts/swarm.sh question <id> add --text "When was the archive mailed?" \
--presumes "The archive was mailed" --why "the client says so"
# what the case takes as given (P-n), when the goal did not designate it
scripts/swarm.sh question <id> premise add --text "The laptop was issued to the subject" \
--locator "the case brief, first paragraph" --why "stated in the brief"
# what the run asks of you, by its id
scripts/swarm.sh requests <id> list --open
scripts/swarm.sh requests <id> authorise R-4
# evidence acquired after the kickoff, and material you supply
scripts/swarm.sh evidence <id> add /evidence/case-42/proxy --why "the proxy export" --for R-4
scripts/swarm.sh material <id> add statement.pdf --why "the client's account"
# lookups, in a dynamic run
scripts/swarm.sh net <id> list
scripts/swarm.sh net <id> grant NR-7 --why "the domain is in the phishing mail"
# a run paused at a cap
scripts/swarm.sh extend <id> --minutes 30 --usd 20
# hold a going run yourself, and let it go on; unpause also tries again
# at once when the model provider's limit paused it
scripts/swarm.sh pause <id> --why "the client asked us to wait"
scripts/swarm.sh unpause <id>
# a run that ended
scripts/swarm.sh resume <id> --question "Who else received the same attachment?"
# after it stops: its process metrics, and its registers replayed under
# this checkout's finish rules, with no model call and nothing written in the run
scripts/swarm.sh metrics <id>
scripts/swarm.sh replay <id>
A person's question is committed to the run's question register first, then posted to the swarm, ranked first in every agent's header and offered to the seat best suited to it. --as names an enrolled person as a claim and --sign signs the act with their key; without either, the act is your OS account's. --presumes says what a question takes as having happened, and that premise is tested before the question is answered. An analyst, enrolled with examiner enroll --role analyst, adds questions and signs no release. amend, priority, withdraw, scope and accept work on a question by its id. premise add designates what the case takes as given, which no answer holds open as a part to prove, and premise admit accepts a premise an agent proposed. How a question is worked
A lead that needs you, evidence the run lacks, a clarification, a lookup to grant and a stop proposed when nothing yields are requests with their own id. --notify hands each new one to a desktop notification, an ntfy topic, a mail address or a command of yours, with its id and kind and nothing of what was asked. requests <id> answer, decline and withdraw close one.
evidence add seals what you acquired as a new inventory revision, answers the request it names, and reopens the leads and answers that rested on the evidence as it was; its files are searched at once for what the standing coverage looked for, and every negative resting on older coverage stays stale until another seat reviews an examination against the addition that says how it bears on the answer, or you accept its question after it. The agents read it at once in their VMs, and through jobs. Under --more-evidence no it is refused, and an agent's ask is answered "no additional input under this case policy". material add supplies a document with who supplied it and when, and every answer resting on it is flagged. The case contract
--network closed is the default. dynamic, in a microVM run, lets an agent ask for one lookup from a reference service, decided by rules under the case policy and fetched on the host; open is every public host. --policy is standard, live_adversary, internal or ctf. net list shows every request, grant and capture, and grant, deny and revoke each take a reason. The network
At the spend cap, the token cap or the wall clock the run pauses and you are told. extend adds to the caps and the seats carry on where they were; stop ends it as stopped. --stop cap-stop at kickoff ends a run nobody watches at its cap, and --until-solved runs until you stop it. pause holds a going run under any policy and unpause lets it go on. When the model provider refuses every seat, the run pauses itself until the time the provider named, or tries every thirty minutes, and tells you once per spell; to free the machine meanwhile, stop it and resume it later. When nothing new appears for a while, the harness proposes a stop as a request, and stops nothing.
resume continues a run that stopped or was sealed, in the same sandbox on the same chains, with the questions you give asked as analyst questions. The clock counts on, the caps grow by what you give, and the next stop seals the continuation anew while every earlier seal and signed release stays valid for what it bound. Caps and continuing
Stuck, or want it run for you?
The repository's troubleshooting page covers the failures we have met. For a case you would rather have worked with you, or a host that has to hold more than yours does, write to us.