Summary
When a custom skill declares its own env var requirements (e.g. MY_API_KEY under forge.env.required) and the user stores the value in the encrypted secrets file (.forge/secrets.enc) via forge secrets set, the value is never loaded into the OS environment. The downstream binary (cli_execute or SkillCommandExecutor) sees the variable as unset, even though the skill correctly declared it.
The runtime works for the same key if it lives in plain .env or in the shell environment — only the encrypted-secrets path is broken.
Root cause
Two functions in forge-cli/runtime/runner.go decide which keys to pull from the configured secrets provider chain, and both iterate a hardcoded allowlist:
Runner.overlaySecrets — forge-cli/runtime/runner.go:2334
OverlaySecretsToEnv — forge-cli/runtime/runner.go:2469
Both contain:
knownKeys := []string{
\"OPENAI_API_KEY\", \"ANTHROPIC_API_KEY\", \"GEMINI_API_KEY\",
\"LLM_API_KEY\", \"MODEL_API_KEY\",
\"TAVILY_API_KEY\", \"PERPLEXITY_API_KEY\",
\"TELEGRAM_BOT_TOKEN\", \"SLACK_APP_TOKEN\", \"SLACK_BOT_TOKEN\",
}
Any skill-declared env var that isn't in this list is silently skipped during overlay, so:
provider.Get(\"MY_API_KEY\") is never called
os.Setenv(\"MY_API_KEY\", ...) never runs
- The downstream executor's
os.Getenv / os.LookupEnv returns empty
Why downstream sees empty values
The executors do receive the correct allowlist of env var names from skill requirements:
SkillCommandExecutor.EnvVars is populated from entry.ForgeReqs.Env.Required/OneOf/Optional — forge-cli/runtime/runner.go:1891-1895
CLIExecuteTool.EnvPassthrough is populated by requirements.DeriveCLIConfig — forge-skills/requirements/derive.go:25-35
But both read values from the process environment:
SkillCommandExecutor.Run — forge-cli/tools/exec.go:67 (os.Getenv(name))
CLIExecuteTool.buildEnv — forge-cli/tools/cli_execute.go:304 (os.LookupEnv(key))
So name-passthrough is correct; only value population from the secrets store is broken.
Behavior matrix for a custom skill env var
| Where the value lives |
Reaches downstream binary? |
OS env (exported before forge run) |
Yes |
.env plain text |
Yes — cmd/common.go:55-58 does os.Setenv per key |
Encrypted secrets file (.forge/secrets.enc) with # MY_API_KEY=<encrypted> placeholder in .env |
No — knownKeys doesn't include it |
This contradicts the contract documented in docs/skills/writing-custom-skills.md:53 ("explicitly declared env vars are passed through").
Reproduction
- Create a skill
skills/my-skill/SKILL.md declaring:
metadata:
forge:
env:
required: [MY_API_KEY]
- Store the value in the encrypted secrets store:
forge secrets set MY_API_KEY=secretvalue
(Leaves # MY_API_KEY=<encrypted> placeholder in .env.)
- Configure
secrets.providers: [encrypted-file] in forge.yaml.
- Run an agent that invokes the skill — the script/binary receives an empty
MY_API_KEY.
Required solution properties
Adding MY_API_KEY to the knownKeys list is not an acceptable fix. Adding a new custom skill must not require any change to runner.go. The fix must dynamically discover which keys to overlay.
Candidate approaches:
- Aggregate from skill requirements. After parsing skill files, union
EnvRequired + EnvOneOf + EnvOptional and use that as the overlay key set (merged with the existing builtin list for backward compatibility). validateSkillRequirements already produces this aggregation — it just needs to flow into the overlay step.
- Use
provider.List(). The secrets.Provider interface already exposes List() ([]string, error) (forge-core/secrets/provider.go:15). Overlay every key the provider returns, optionally intersected with the skill-declared set for safety.
Either approach must also ensure the overlaid values are written into the OS environment (os.Setenv), not just into the in-runner envVars map, because the executors read via os.Getenv / os.LookupEnv.
Affected files (pointers, not a fix list)
forge-cli/runtime/runner.go:2334 — Runner.overlaySecrets (mutates envVars map only)
forge-cli/runtime/runner.go:2469 — OverlaySecretsToEnv (writes to OS env)
forge-cli/cmd/common.go:51-77 — call site; runs before Runner.Run
forge-cli/tools/exec.go:67 — SkillCommandExecutor reads os.Getenv
forge-cli/tools/cli_execute.go:304 — CLIExecuteTool reads os.LookupEnv
forge-skills/requirements/derive.go — produces the per-executor passthrough lists
Acceptance criteria
- A skill declaring an arbitrary env var name (not in any builtin list) can have its value stored via
forge secrets set and have that value visible to its downstream script/binary at runtime.
- No edits to
runner.go (or any other file) are required when adding a new skill with new env var names.
- Existing builtin-key behavior (LLM/search/channel tokens) is preserved.
- Cross-category secret-reuse detection in
overlaySecrets continues to work for the builtin set.
Summary
When a custom skill declares its own env var requirements (e.g.
MY_API_KEYunderforge.env.required) and the user stores the value in the encrypted secrets file (.forge/secrets.enc) viaforge secrets set, the value is never loaded into the OS environment. The downstream binary (cli_executeorSkillCommandExecutor) sees the variable as unset, even though the skill correctly declared it.The runtime works for the same key if it lives in plain
.envor in the shell environment — only the encrypted-secrets path is broken.Root cause
Two functions in
forge-cli/runtime/runner.godecide which keys to pull from the configured secrets provider chain, and both iterate a hardcoded allowlist:Runner.overlaySecrets—forge-cli/runtime/runner.go:2334OverlaySecretsToEnv—forge-cli/runtime/runner.go:2469Both contain:
Any skill-declared env var that isn't in this list is silently skipped during overlay, so:
provider.Get(\"MY_API_KEY\")is never calledos.Setenv(\"MY_API_KEY\", ...)never runsos.Getenv/os.LookupEnvreturns emptyWhy downstream sees empty values
The executors do receive the correct allowlist of env var names from skill requirements:
SkillCommandExecutor.EnvVarsis populated fromentry.ForgeReqs.Env.Required/OneOf/Optional—forge-cli/runtime/runner.go:1891-1895CLIExecuteTool.EnvPassthroughis populated byrequirements.DeriveCLIConfig—forge-skills/requirements/derive.go:25-35But both read values from the process environment:
SkillCommandExecutor.Run—forge-cli/tools/exec.go:67(os.Getenv(name))CLIExecuteTool.buildEnv—forge-cli/tools/cli_execute.go:304(os.LookupEnv(key))So name-passthrough is correct; only value population from the secrets store is broken.
Behavior matrix for a custom skill env var
forge run).envplain textcmd/common.go:55-58doesos.Setenvper key.forge/secrets.enc) with# MY_API_KEY=<encrypted>placeholder in.envknownKeysdoesn't include itThis contradicts the contract documented in
docs/skills/writing-custom-skills.md:53("explicitly declared env vars are passed through").Reproduction
skills/my-skill/SKILL.mddeclaring:# MY_API_KEY=<encrypted>placeholder in.env.)secrets.providers: [encrypted-file]inforge.yaml.MY_API_KEY.Required solution properties
Adding
MY_API_KEYto theknownKeyslist is not an acceptable fix. Adding a new custom skill must not require any change torunner.go. The fix must dynamically discover which keys to overlay.Candidate approaches:
EnvRequired + EnvOneOf + EnvOptionaland use that as the overlay key set (merged with the existing builtin list for backward compatibility).validateSkillRequirementsalready produces this aggregation — it just needs to flow into the overlay step.provider.List(). Thesecrets.Providerinterface already exposesList() ([]string, error)(forge-core/secrets/provider.go:15). Overlay every key the provider returns, optionally intersected with the skill-declared set for safety.Either approach must also ensure the overlaid values are written into the OS environment (
os.Setenv), not just into the in-runnerenvVarsmap, because the executors read viaos.Getenv/os.LookupEnv.Affected files (pointers, not a fix list)
forge-cli/runtime/runner.go:2334—Runner.overlaySecrets(mutatesenvVarsmap only)forge-cli/runtime/runner.go:2469—OverlaySecretsToEnv(writes to OS env)forge-cli/cmd/common.go:51-77— call site; runs beforeRunner.Runforge-cli/tools/exec.go:67—SkillCommandExecutorreadsos.Getenvforge-cli/tools/cli_execute.go:304—CLIExecuteToolreadsos.LookupEnvforge-skills/requirements/derive.go— produces the per-executor passthrough listsAcceptance criteria
forge secrets setand have that value visible to its downstream script/binary at runtime.runner.go(or any other file) are required when adding a new skill with new env var names.overlaySecretscontinues to work for the builtin set.