Bro Best Practices
Bro's cache is only as good as your inputs declarations. A fingerprint covers exactly what you declare — command, env, input file contents, input_cmds output, and dependency outputs — so the rules below are about declaring neither too little (stale cache hits) nor too much (perpetual misses, slow fingerprints).
Declare inputs completely — or accept stale results
If a file can change the result of a task, it must be reachable from that task's inputs (or from a dependency's outputs). Bro never guesses: undeclared files are invisible to the fingerprint, so editing them reuses the old cache entry. When in doubt, run with --verbose and check which files were hashed.
Use lockfiles as proxies — never glob dependency directories
# BAD: hashes tens of thousands of files on every run
inputs: ["src/**/*.ts", "node_modules/**/*"]
# GOOD: the lockfile content pins the whole dependency tree
inputs: ["src/**/*.ts", "package.json", "package-lock.json"]The same rule applies everywhere: go.mod/go.sum for Go, requirements.txt/poetry.lock/uv.lock for Python, Cargo.lock for Rust, Gemfile.lock for Ruby. The lockfile is a content hash of your dependency set — hashing it is one file read instead of a tree walk, and it cannot miss a change that matters.
Never let a task's inputs overlap its own outputs
# BAD: out/report.txt is both an output and (via out/**) an input
tasks:
report:
cmd: "generate > out/report.txt"
inputs: ["data/**", "out/**"]
outputs: ["out/report.txt"]Every run rewrites the output, which changes the inputs, which changes the fingerprint — a guaranteed cache miss forever. Keep inputs to source files; downstream tasks should consume another task's outputs through deps, whose output hashes enter the fingerprint automatically.
The cache directory itself (.bro/cache) is always excluded from input expansion, so broad patterns like **/* are safe in that respect — but they still fold your outputs (and anything else new) into the fingerprint. Prefer specific patterns.
Capture toolchain versions with input_cmds — and keep them fast
tasks:
build:
cmd: "go build -o bin/app ."
inputs: ["**/*.go", "go.mod", "go.sum"]
input_cmds: ["go version"]
outputs: ["bin/app"]input_cmds stdout is part of the fingerprint, so a compiler upgrade correctly invalidates the cache. But they run on every fingerprint computation — including cache hits — so they must be fast and side-effect-free. go version, node --version, protoc --version are fine; anything that hits the network or takes more than a few milliseconds will quietly tax every single run. A failing input_cmds command aborts the run with an error — it is never silently skipped.
Set cache: false on tasks with side effects
Deployment, publishing, database migrations, rm -rf style cleanups — anything whose effect is outside its declared outputs must not be cached:
tasks:
deploy:
deps: ["build"]
cmd: "./scripts/deploy.sh"
cache: falseTasks with no inputs and no input_cmds already default to uncached (three-state auto), but an explicit cache: false documents intent and survives later edits that add an inputs field. Conversely, almost never use cache: true on a side-effecting task: Bro will happily replay recorded output and restore nothing, making "deployed" indistinguishable from "deployed yesterday".
Keep watch-mode limitations in mind
bro watch <task>monitors task inputs, notbro.yamlitself — after editing the config, restart the watch session.- Watch passes no task arguments; parameterized tasks (
{{args}}/{{argN}}placeholders) are not supported in watch mode.
Remote cache credential hygiene
The remote cache trusts anyone who can write to it (spec Section 9.5): checksums protect against transmission corruption, not against a malicious writer poisoning entries.
- In CI, download with read-only credentials; use separate, tightly scoped write credentials only in the job that uploads.
- Never put credentials in
bro.yaml. S3 credentials come fromAWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY(or the MinIO equivalents); the HTTP backend uses an optional Bearer token fromBRO_REMOTE_HTTP_TOKEN. Inject them as CI secrets. - Use a per-project
prefix:so projects sharing a bucket cannot collide.
Known limitations
- Output archives are tar+zstd; the decoder tolerates trailing garbage after a valid zstd frame, so integrity checks rely on the entry layout and metadata fingerprint match rather than byte-exact archive length.
- Config discovery is current-directory only: run
brofrom the directory containingbro.yaml. bro runhas no--forceflag; to force re-execution, change an input orbro cleanthe cache.