Skip to content

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

yaml
# 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

yaml
# 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

yaml
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:

yaml
tasks:
  deploy:
    deps: ["build"]
    cmd: "./scripts/deploy.sh"
    cache: false

Tasks 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, not bro.yaml itself — 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 from AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY (or the MinIO equivalents); the HTTP backend uses an optional Bearer token from BRO_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 bro from the directory containing bro.yaml.
  • bro run has no --force flag; to force re-execution, change an input or bro clean the cache.

MIT License