# Configuration References > Refer to variables, secrets, and MCP servers with config.* without embedding values in workflows. Workflows should describe *what* to do, not carry the credentials and settings needed to do it. The `config` namespace references external configuration; the platform resolves the effective values at execution time. ## References
Reference Meaning Types as
config.var("name") A configuration variable (e.g. a target domain, a severity filter) string
config.secret("name") A secret (API keys, tokens) string
config.mcp("name") A configured MCP server connection for agent steps MCP ref
```bhw step.exec( command = ["nuclei", "-l", "live.txt", "-severity", config.var("nuclei_severity"), "-o", "findings.txt"], env = {"GITHUB_TOKEN": config.secret("github_token")}, ) ``` The scope of a name — global, organization, project, or environment — is managed by the platform. Your workflow stays identical across environments; only the configuration differs. ## Secrets Are Never Serialized Only the **reference** is stored — in the workflow definition, the AST, the serialized steps, execution state, and logs. Secret values are resolved at execution time and never written anywhere your workflow can leak them: ```bhw env = {"PUSHOVER_TOKEN": config.secret("pushover_token")} ``` What gets stored is `config.secret("pushover_token")` — not the token itself. Sensitivity propagates through composition: anything derived from a secret (concatenation, trimming, casing) is still treated as sensitive. ## Lazy Strings Compose Config references are lazy strings: they type as `string` but hold an expression tree that evaluates at execution time. You can compose them freely: ```bhw # Concatenation builds up a target URL from configuration. step.exec( command = [ "katana", "-u", "https://" + config.var("target_domain"), "-d", "3", ], ) # String methods work on references too. step.exec( command = ["subfinder", "-d", config.var("target_domain").trim(), "-silent", "-o", "subs.txt"], ) ``` Because these are references, changing the target domain for a program is a configuration change — not an edit to every workflow you own. ## Realistic Configuration Layout A typical hunting setup keeps per-program settings in configuration:
Config name Example use
target_domain "acme-example.com" — passed to subfinder, httpx, nuclei
program Program slug recorded into datasets as provenance
nuclei_severity "critical,high" — tune without editing workflows
slack_webhook / notification tokens ( secret ) Where findings get announced
github_token ( secret ) For agents using the GitHub MCP server
```bhw workflow( name = "parametrized-recon", activities = [ activity( name = "discover", triggers = [trigger.cron("0 0 * * *")], steps = [ step.exec(command = ["subfinder", "-d", config.var("target_domain"), "-silent", "-o", "subs.txt"]), step.transform( source = path.file("subs.txt").lines(), pipeline = transform.trim().unique().sort(), destination = path.file("subs-clean.txt"), ), step.update_dataset( table = "subdomains", source = path.file("subs-clean.txt").lines(), pipeline = transform.unique(), mapping = {"host": row.value, "source": "subfinder"}, mode = "upsert", ), ], ), ], ) ``` The same workflow runs against any program: point `target_domain` and `program` at different values and clone it per target. ## Names Reference names must be non-empty after trimming. Unknown or unresolved names surface at execution time with a clear error, so typos fail loudly instead of silently scanning the wrong thing.