Bundles
Pack a flow and everything it runs with into one .heddle file anyone can run.
A flow is rarely just a flow. By the time an agent works it is a spec and a directory of tools, a plugin or two, a folder of files the tools read, and the settings its middleware runs with: five paths on your machine, held together by one shell command only you have. A bundle is that command made into a file:
heddle bundle spec.yaml --tools-dir tools --plugin plugin.json \
--mount skills --input '{"query":"hello"}' -o agent.heddleSend agent.heddle to anyone. Where heddle is installed, it runs with nothing else:
heddle run agent.heddleWhat goes in
heddle bundle takes the same flags a run does, and packs what each names:
| Flag | What travels |
|---|---|
<flow> | The spec file, JSON or YAML |
--tools-dir <dir> | The tool executables, with their executable bits, read back exactly as a run reads a tools directory |
--plugin <manifest> | The plugin's whole directory. A manifest plugin may reference only what is inside its own directory, and that confinement rule is exactly what makes it shippable |
--mount <src[:dest][:ro|:rw]> | The file or directory, with where it lands and whether changes copy back |
--plugin-config <Type>=<json> | The settings, resolved now; a @file value is read at pack time, so only its contents travel |
--input <json> | A default input, recorded so the receiver's first run can be zero-configuration |
--requires <json|@file> | What the receiving machine must already have — checked before a run starts, never acted on. See below |
The bundle is checked before it is written: the spec must parse, the graph must compile
and validate, and every tool the flow names must be carried by --tools-dir or a
--plugin. A bundle that packs is a bundle that runs. The failure your recipient would
have hit is the failure you get now, on the machine that can fix it.
Running one
heddle run and heddle validate both take a .heddle where they take a flow. The rule
for flags is: the bundle proposes, the command line disposes.
heddle run agent.heddle # the recorded input
heddle run agent.heddle --input '{"query":"mine"}' # yours wins
heddle run agent.heddle --mount ./more-data # composes with the bundle's
heddle run agent.heddle --session support-42 # sessions work as alwaysA singular flag you pass overrides what the bundle recorded; a repeatable one composes
with it, the bundle's entries first. A --plugin-config for a component type the bundle
also configures replaces that type's settings entirely.
A bundle does not have to be a file you already have. An https:// address is fetched to
a temporary file and opened the same way, and a bare name — no dots, no path separators —
is shorthand for the library's copy of that entry:
heddle run https://heddle.run/library/coding-agent.heddle # fetched, then run as above
heddle run coding-agent # the same, spelled shortThe disk is consulted first, so a file or directory that answers to the name keeps meaning
itself. And only .heddle archives travel by URL: a bare flow file names tools and mounts
by paths that did not come with it, so the archive is the form worth an address.
What is inside
A .heddle is a gzipped tar archive with a heddle.json manifest at its root. No
custom container, no dependency acquired to read or write it; heddle carries its own
ustar reader and writer over node:zlib. The practical consequence: you can inspect a
bundle you were handed with tools you already trust, before heddle touches it.
$ tar -tzf agent.heddle
heddle.json
flow/spec.yaml
tools/
tools/bash.py
tools/read_file.py
plugins/skills/
plugins/skills/plugin.json
plugins/skills/bin/list_skills.py
mounts/0/
mounts/0/date-arithmetic.mdheddle.json names everything else, so the layout is data rather than convention:
{
"format": 1,
"name": "skills",
"flow": "flow/spec.yaml",
"tools": "tools",
"plugins": ["plugins/skills/plugin.json"],
"pluginConfig": { "RetryPolicy": { "maxAttempts": 3 } },
"mounts": [{ "path": "mounts/0", "dest": "skills", "mode": "ro" }],
"input": { "query": "what skills do you have?" }
}format is the version. A bundle from a newer heddle is refused with an upgrade hint
rather than half-read.
What deliberately does not travel
A bundle carries what an agent is, not what an operator decided. Four things are left out on purpose, and asking for them is asking the wrong file:
- Credentials. A spec references a key as
$ENV_VARand it resolves on the machine that runs, never the one that packed. Nothing in a bundle can carry a secret unless you put one in a mounted file yourself, so don't. - Sandbox policy.
--safe,--allow-read,--deny-netare the receiving operator's judgment about their own machine. A file that arrived in the mail does not get to make it. - Sessions. A conversation is machine-local state in
~/.heddle/sessions/. A bundle runs in a session (--sessionworks as always); it does not bring one. - Discovery grants. A bundled plugin declaring
discoverToolsstill needs--discover-toolsfrom whoever runs it, because that flag means "you may start this plugin's process", and only the person running can grant it.
One thing cannot travel: an in-process plugin, a --plugin that is an importable
module rather than a .json manifest. What it imports lives in the author's
node_modules, so its closure is not something a directory walk can collect, and a bundle
that shipped only the entry file would run on one machine in the world. heddle bundle
refuses it and points at the manifest format, which is the
shippable kind.
What a bundle needs from your machine
Some agents want more than heddle. local-notetaker records with binaries it does not
ship, transcribes with a model file it does not carry, and writes up the notes with a
key that is yours. A bundle can say so:
heddle bundle spec.yaml --tools-dir tools --requires '[
{"binary": "ffmpeg", "hint": "brew install ffmpeg"},
{"binary": "whisper-cli", "hint": "brew install whisper-cpp"},
{"env": "OPENAI_API_KEY", "hint": "for the notes step"},
{"file": "~/models/ggml-base.en.bin", "hint": "see the README for the download"},
{"node": ">=22"}
]' -o agent.heddleThe list travels in heddle.json, and heddle run checks it before it starts anything —
the same moment it checks that the tools resolve. Everything missing is reported at once:
local-notetaker cannot run here — 2 requirements unmet:
✗ whisper-cli not on PATH brew install whisper-cpp
✗ OPENAI_API_KEY not set for the notes step
✓ ffmpeg, node v22.17.0That is the whole point of it being a list. Without one you fix a machine one failure at
a time, and each failure arrives later than the last — the missing binary mid-run, the
missing key at the first model call. heddle doctor agent.heddle prints exactly this without running
anything, and exits non-zero when something is missing, so CI can ask the same question.
On a terminal, heddle run turns the report into a pause rather than an exit. A missing
environment variable is asked for first, hidden as you type and kept for that run only —
the same prompt a spec's $OPENAI_API_KEY gets. For a few well-known requirements —
whisper-cli, ffmpeg, the whisper model files — heddle knows the fix itself and offers
to make it, the exact commands on screen first, acting only on a typed y; every command
and URL in that offer comes from a table versioned with the CLI, never from the bundle
(see the callout below). Whatever is left waits: run the install in another shell, press
enter, and heddle checks again; q stops with the failure CI would have seen. In a
script or a pipeline nothing is asked, nothing is offered, and the refusal is immediate,
so automation sees exit codes, never questions.
There are four kinds of requirement, and each is a pure observation of your machine:
| Kind | Holds when | Notes |
|---|---|---|
{"binary": "name"} | the name resolves on $PATH | a list means any one of them will do; an entry containing / is checked where it points, for the app-bundle paths a Mac needs |
{"env": "NAME"} | the variable is set and non-empty | the value is never read into the report — only whether it is set |
{"file": "path"} | the path exists | a leading ~ is your home directory |
{"node": ">=22"} | the running Node satisfies it | one >= against a version; heddle carries no semver library and will not pretend to. Anything else is refused when the bundle is packed |
hint is free text shown beside the failure it explains. It is a string for a human to
read — nothing passes it to a shell.
A requirement is a thing to look for, never a thing to run. There is deliberately no
setup, install or preRun field, and the check itself installs, downloads and
executes nothing: a .heddle can be downloaded from anywhere, and a bundle that ran
commands on open would be remote code execution by design. When heddle run offers to
install something, the declaration has only selected an entry from the CLI's own
reviewed table — the command, the URL and the destination are heddle's words, shown to
you and run only after your y. A bundle cannot author an install, and a hint is still
a string for a human to read. Everything the table does not cover is yours to install,
with your own package manager, after reading what the report said.
The declaration is the author's description of a machine they have never seen, so the
operator keeps the last word: heddle run --no-preflight starts anyway. It is the same
bargain as --discover-tools — the bundle declares, and the person at the keyboard
decides.
Nothing breaks for bundles without the field, in either direction. A bundle that declares nothing is checked for nothing, and an older heddle handed a bundle that declares requirements ignores the field and runs as it always did — which is why adding it did not bump the format version.
Receiving one
A bundle is somebody else's code, packaged conveniently. Treat it exactly as you would a repository you just cloned. heddle's side of that bargain:
- Extraction refuses path traversal, absolute paths, and symlinks or hard links anywhere in the archive, and caps what a bundle may unpack to, so opening one is safe.
- Reading a plugin manifest runs nothing, so
heddle validate agent.heddleinspects the whole bundle, spec and graph and tools and plugin manifests, without executing a line of it. heddle doctor agent.heddleanswers "would this work here?" the same way — by looking at your machine, not by touching the bundle's code.
Running it is running its tools, like any tools directory. The same advice applies, with more reason:
heddle validate agent.heddle # look first
tar -tzf agent.heddle # or with your own eyes
heddle run agent.heddle --safe # then run it confinedTools in a bundle are programs, and without --safe a tool is a subprocess with your
whole environment, API keys included. For a bundle you did not build, --safe is not
paranoia; it is the default posture toward executable mail. See
Sandboxing.
Bundles to start from
The library is a set of ready-made ones — meeting notes, issue triage, docs Q&A, a CSV analyst, a changelog writer. Each is a directory in the repository that packs with one command, so they are worth reading as worked examples of the flags above as much as for running: one has no tools at all, one carries two tools and a folder of documents, one branches three ways.
Reference
The full flag table lives in the CLI reference.