go:embed: bundling assets and rule sets into binaries
Shipping a CLI that depends on data files is where packaging quietly goes wrong. The binary builds fine, but the rules it needs — regex patterns, default configs, license texts — live in files that must exist at a specific path on every machine that runs it. The first time a user installs the tool and the files are not where the code expects them, you discover the real cost of a binary that is not self-contained.
What go:embed does
The go:embed directive, added in Go 1.16, imports files or directories into the compiled program as an embedded filesystem. Instead of reading from disk at runtime, the code reads from data compiled into the binary:
//go:embed rules/*.toml
var ruleFS embed.FS
The pattern is validated at build time: if the files do not exist, the build fails. That single property changes the failure mode. Missing assets stop being a runtime surprise and become a compile error, which is where you want to find out.
The rules of the game
go:embed is deliberately constrained, and the constraints are worth knowing:
- Patterns are relative to the source file containing the directive — no
.., no absolute paths. - Files can be embedded individually, by glob, or by directory. The
all:prefix includes files normally skipped, such as those starting with.or_. - Directories are exposed as an
fs.FS, giving you the standard file system interface — open, read, walk — without ever touching the real filesystem. - The directive must sit directly above a declaration of type
string,[]byte, orembed.FS; you cannot populate an embedded variable later.
Rule sets are the perfect use case
Security tooling is where embedding earns its keep. A DLP or secret-scanning tool needs a list of detection patterns — regexes for API keys, tokens, and credentials. If those patterns ship as loose files, three bad things happen. First, the tool and its rules can drift apart: a user updates the binary but keeps an old rules directory, or the reverse. Second, installers must decide where the rules live, which means documenting paths and debugging permission errors. Third, tampering becomes possible — a modified rules file silently changes what the tool detects.
Embedded rules fix all three. The patterns are part of the binary, so the tool and its rules always match, there is no install step, and modifying the rules means rebuilding the binary, which is a visible act. Rule sets of a few hundred kilobytes embed without trouble; teams with larger sets can ship the embedded set as a secure default and layer an override path for custom rules on top.
When the binary is the product
For a single-static-binary tool, embedding is not an optimization — it is what makes the packaging story true. If you distribute one file, every asset the tool needs at runtime has to be inside that file.
trustless, a credential broker CLI for AI agents distributed as a single static binary, bundles its DLP engine’s 40 gitleaks-derived detection rules (MIT-licensed, with attribution in NOTICE) so the redaction patterns and the tool can never drift apart — and go:embed is the idiomatic mechanism for exactly this job. One binary, one rule set, nothing to install alongside.
The go:embed directive turns “where do the files go on install” into a compile-time question. Next time you reach for an external assets directory, ask yourself: what breaks on this machine if those files are missing, and would you rather find out at build time or on a user’s laptop?