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