Uh oh!
There was an error while loading. Please reload this page.
Allow secrets in step-level env: bindings under strict mode - #25779
Conversation
Strict mode blocks all secrets.* in user-defined steps, but step-level env: bindings are controlled, masked surfaces — functionally equivalent to what the framework's own generated jobs do. The validation now classifies secrets by location within each step: - env: bindings → allowed (masked by Actions, scoped to step) - run:, with:, other fields → still blocked (inline interpolation) Add classifyStepSecrets() helper, update validateStepsSectionSecrets(), improve error message to suggest env: bindings as alternative. Refs: #25747 Agent-Logs-Url: https://github.com/github/gh-aw/sessions/cbccafb7-fb26-495b-85dc-786315599518 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
pelikhan
commented
Apr 11, 2026
@copilot steps are not allowed to reference $GITHUB_ENV to avoid leaking secrets in env |
Steps must not reference $GITHUB_ENV to avoid leaking secrets from env: bindings to subsequent steps. Updated test examples to use tool invocation patterns that consume secrets only within the step. Agent-Logs-Url: https://github.com/github/gh-aw/sessions/de761e54-6d1f-4961-8f74-865ec8a05cc9 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Fixed in 22cd2b0 — removed all |
There was a problem hiding this comment.
Pull request overview
Enables strict mode workflows to use ${{ secrets.* }} in step-level env: bindings while still blocking secrets in other step fields (e.g., run, with) to reduce accidental secret leakage into the agent job.
Changes:
- Updated strict-mode steps validation to allow secrets referenced only within step
env:maps. - Added
classifyStepSecrets()helper to partition secret refs into env-bound vs unsafe refs (with malformedenvtreated as unsafe). - Added/updated unit tests and introduced integration tests covering compilation behavior; added an ADR documenting the decision.
Show a summary per file
| File | Description |
|---|---|
| pkg/workflow/strict_mode_steps_validation.go | Implements env-vs-unsafe secret classification and updates strict/non-strict enforcement & messaging. |
| pkg/workflow/strict_mode_steps_validation_test.go | Updates existing tests and adds unit tests for classifyStepSecrets(). |
| pkg/workflow/strict_mode_steps_secrets_integration_test.go | Adds integration coverage for strict-mode compilation allowing env-bound secrets while blocking unsafe secrets. |
| docs/adr/0001-allow-secrets-in-step-env-bindings-under-strict-mode.md | Documents the design decision and normative behavior for secret classification/enforcement. |
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 4/4 changed files
- Comments generated: 5
| if c.strictMode { | ||
| // In strict mode, secrets in step-level env: bindings are allowed | ||
| // (controlled binding, masked by GitHub Actions). Only block secrets | ||
| // found in other fields (run, with, etc.). | ||
| if len(unsafeSecretRefs) == 0 { | ||
| strictModeValidationLog.Printf("All secrets in %s section are in env bindings (allowed in strict mode)", sectionName) | ||
| return nil |
There was a problem hiding this comment.
Allowing secrets in step-level env under strict mode introduces a new, easy-to-miss leak path: a step can persist an env-bound secret to later steps (including the agent step) via "$GITHUB_ENV" (or equivalent), which defeats the “step-only” scoping described in the PR. Consider adding strict-mode validation that rejects steps which both (a) contain env-bound secrets and (b) reference GITHUB_ENV / write to it in run (and/or documenting this explicitly in the compiler error/warning output).
| unsafeSecretRefs = sliceutil.Deduplicate(unsafeSecretRefs) | ||
| return fmt.Errorf( | ||
| "strict mode: secrets expressions detected in '%s' section may be leaked to the agent job. Found: %s. "+ | ||
| "Operations requiring secrets must be moved to a separate job outside the agent job", | ||
| sectionName, strings.Join(secretRefs, ", "), | ||
| "Operations requiring secrets must be moved to a separate job outside the agent job, "+ | ||
| "or use step-level env: bindings instead", | ||
| sectionName, strings.Join(unsafeSecretRefs, ", "), | ||
| ) |
There was a problem hiding this comment.
The ordering of secret refs in error/warning messages can be nondeterministic because refs are collected via Go map iteration (stepMap iteration, nested map traversal, and ExtractSecretsFromValue returns a map). This can lead to unstable error strings and flaky tests/assertions downstream. Consider producing deterministic output by sorting the deduplicated ref slices (unsafeSecretRefs / allSecretRefs) before strings.Join.
| # ADR-0001: Allow Secrets in Step-Level env: Bindings Under Strict Mode | ||
| **Date**: 2026-04-11 | ||
| **Status**: Draft | ||
| **Deciders**: pelikhan, Copilot |
There was a problem hiding this comment.
This ADR is titled/numbered "ADR-0001" but the repository already contains another ADR-0001 file (docs/adr/0001-conditional-oidc-env-var-forwarding-to-mcp-gateway.md). ADR IDs should be unique; please renumber this document (and its filename/header) to the next available number to avoid ambiguity in references.
| 1. Implementations **MUST** classify each `${{ secrets.* }}` reference within a step by the name of the YAML field in which it appears. | ||
| 2. A secret reference found inside the `env:` field of a step **MUST** be classified as an *env-bound* reference. | ||
| 3. A secret reference found in any other step field (including but not limited to `run:`, `with:`, `name:`, `if:`) **MUST** be classified as an *unsafe* reference. | ||
| 4. A step value that is not a YAML map (e.g., a raw string) **MUST** treat all secret references within it as *unsafe* references. | ||
| 5. Implementations **MUST NOT** treat an *env-bound* reference as *unsafe*, nor vice versa. |
There was a problem hiding this comment.
Normative spec mismatch with implementation: the code intentionally treats malformed env: values (string/slice) as unsafe to prevent bypass, but the spec currently says any secret reference in env: MUST be env-bound and MUST NOT be classified as unsafe. Update the spec to explicitly scope “env-bound” to well-formed env mappings (or otherwise reconcile with the malformed-env-as-unsafe behavior).
| // Filter out the built-in GITHUB_TOKEN: it is already present in every runner | ||
| // environment and is not a user-defined secret that could be accidentally leaked. | ||
| secretRefs = filterBuiltinTokens(secretRefs) | ||
| unsafeSecretRefs = filterBuiltinTokens(unsafeSecretRefs) | ||
| envSecretRefs = filterBuiltinTokens(envSecretRefs) | ||
There was a problem hiding this comment.
filterBuiltinTokens currently drops any ref whose expression string contains the substring secrets.GITHUB_TOKEN. This can unintentionally exempt user secrets like secrets.GITHUB_TOKEN_SUFFIX, and (more critically) can cause other secrets to be ignored when they appear in the same expression as GITHUB_TOKEN (because the entire expression gets filtered). Consider filtering more precisely by parsing secret names from the expression and only removing refs when the expression contains onlyGITHUB_TOKEN (or filter by secret name before collapsing to expression strings).
✅ Design Decision Gate — ADR VerifiedThe implementation in this PR aligns with the stated Architecture Decision Record. ADR reviewed: ADR-0001: Allow Secrets in Step-Level env: Bindings Under Strict Mode Verification Summary
The code faithfully implements every MUST and MUST NOT requirement in the ADR's normative specification. 🏗️
|
There was a problem hiding this comment.
Implementation verified: code aligns with the linked Architecture Decision Record (ADR-0001: Allow Secrets in Step-Level env: Bindings Under Strict Mode).
All MUST and MUST NOT normative requirements are satisfied. Two housekeeping items remain before merge:
- Rename the ADR file to
0002-*.mdto resolve the numbering collision with the existing0001-conditional-oidc-env-var-forwarding-to-mcp-gateway.md. - Update the ADR Status from
DrafttoAccepted.
🧪 Test Quality Sentinel ReportTest Quality Score: 70/100
Test Classification Details
Suggestions — Not Blocking
|
There was a problem hiding this comment.
✅ Test Quality Sentinel: 70/100. Test quality is acceptable — 0% of new tests are implementation tests (threshold: 30%). All 6 test functions verify observable behavioral contracts, both files have correct build tags, and no forbidden mock libraries are used. Minor suggestions posted in the comment above.
pelikhan
commented
Apr 11, 2026
@copilot review comments |
…, ADR renumbering, precise token filter - Add GITHUB_ENV leak detection: steps with env-bound secrets that reference GITHUB_ENV in run: are reclassified as unsafe - Sort secret refs before joining for deterministic error messages - Renumber ADR from 0001 to 0002 to avoid conflict with existing ADR - Update ADR spec to scope "env-bound" to well-formed env mappings and document GITHUB_ENV reclassification rule - Fix filterBuiltinTokens to use precise secret name parsing instead of substring matching (secrets.GITHUB_TOKEN_SUFFIX no longer exempt) - Add tests for all new behaviors Agent-Logs-Url: https://github.com/github/gh-aw/sessions/8b6d69ab-d376-4ef9-a150-520c283b33b4 Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Addressed all 5 review comments in f47d1f2:
|
Strict mode blocks all
secrets.*in user-defined steps, forcing workflows that need tool credentials (API tokens, OAuth keys) to disable strict mode entirely viastrict: false, losing all other protections. Step-levelenv:bindings are controlled, masked surfaces — functionally equivalent to what the framework's own generated jobs already do.Important: Secrets in
env:bindings must be consumed only within the step itself. Steps that reference$GITHUB_ENVwhile using env-bound secrets are blocked to prevent leaking secrets to subsequent steps (including the agent step).Changes
classifyStepSecrets()— new helper partitions a step's secret refs intounsafeRefs(run, with, etc.) andenvRefs(env bindings). Malformed env values (string, slice) are treated as unsafe to prevent bypass. Steps that reference$GITHUB_ENVwhile having env-bound secrets are reclassified as entirely unsafe.validateStepsSectionSecrets()— now only errors onunsafeRefsin strict mode; env-only secrets pass throughfilterBuiltinTokens()— now parses secret names precisely via regex instead of substring matching;secrets.GITHUB_TOKEN_SUFFIXand mixed expressions are no longer wrongly exemptstepReferencesGitHubEnv()/valueReferencesGitHubEnv()— new helpers detect$GITHUB_ENVreferences in non-env step fields to prevent secret leakageenv:bindings as alternativefilterBuiltinTokensprecision (5 cases),stepReferencesGitHubEnv(3 cases), updated validation tests (9 new cases), integration tests for full compilationExample
Now compiles under strict mode (secret scoped to the step only):
Still blocked (inline secret in run):
Also blocked (env-bound secret leaked via
$GITHUB_ENV):