Skip to content

Skill-declared env vars stored as encrypted secrets are not passed to downstream binaries #48

@initializ-mk

Description

@initializ-mk

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.overlaySecretsforge-cli/runtime/runner.go:2334
  • OverlaySecretsToEnvforge-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/Optionalforge-cli/runtime/runner.go:1891-1895
  • CLIExecuteTool.EnvPassthrough is populated by requirements.DeriveCLIConfigforge-skills/requirements/derive.go:25-35

But both read values from the process environment:

  • SkillCommandExecutor.Runforge-cli/tools/exec.go:67 (os.Getenv(name))
  • CLIExecuteTool.buildEnvforge-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 NoknownKeys 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

  1. Create a skill skills/my-skill/SKILL.md declaring:
    metadata:
      forge:
        env:
          required: [MY_API_KEY]
  2. Store the value in the encrypted secrets store:
    forge secrets set MY_API_KEY=secretvalue
    
    (Leaves # MY_API_KEY=<encrypted> placeholder in .env.)
  3. Configure secrets.providers: [encrypted-file] in forge.yaml.
  4. 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:

  1. 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.
  2. 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:2334Runner.overlaySecrets (mutates envVars map only)
  • forge-cli/runtime/runner.go:2469OverlaySecretsToEnv (writes to OS env)
  • forge-cli/cmd/common.go:51-77 — call site; runs before Runner.Run
  • forge-cli/tools/exec.go:67SkillCommandExecutor reads os.Getenv
  • forge-cli/tools/cli_execute.go:304CLIExecuteTool 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions