You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A maintainer sets up a third-party model provider in .prflow/config.json. That provider entry has dedicated fields for the endpoint (base_url) and the request timeout (timeout_ms), and a free-form env map for anything else. If the maintainer puts ANTHROPIC_BASE_URL or API_TIMEOUT_MS in the env map, it quietly beats the dedicated field, and nothing in the run says so. Two names that are not written by the step at all, HOME and RUNNER_TEMP, are just as unguarded, and setting one of them moves where every later step in the job looks for its files.
This is a config footgun, not a security hole. .prflow/config.json is maintainer-owned and read from a trusted branch, and the two names that would matter for credentials are already refused.
Current Behavior
Triggering input. A provider entry whose env map names any of ANTHROPIC_BASE_URL, API_TIMEOUT_MS, HOME or RUNNER_TEMP.
Observed result. The "Inject provider endpoint" step exits 0 and prints nothing. It writes its own values first and appends the env map last, so the map's value is the one later steps see. On the Bedrock path the run contradicts itself: the step warns that base_url is ignored, while the map puts an endpoint back.
Expected result. A warning naming the keys, so the maintainer can see that the env map decided the value.
Environment. All three cloud workflows, on the GitHub-hosted Actions runner. The defect does not depend on the operating system or the shell.
Desired Behavior
The step keeps working exactly as it does today, and adds one warning. When a provider env map names any of the four watched keys, the step prints a single ::warning:: listing every matched key and carries on. Which value ends up in effect does not change: the env map still wins, deliberately, so no existing setup breaks.
User Impact
A maintainer who sets one of these keys in the env map finds out from the run log instead of from surprising behavior. Everyone else sees no change: a run whose env map names none of the four keys behaves and logs exactly as before.
Technical Context
Scope note: The files and details below are the known starting points, not the full list. Before implementing, trace the change through the codebase to find every affected call site, consumer, and layer — this issue maps the work, it does not bound it.
Relevant Classes/Files — the "Inject provider endpoint (provider-routed sections only)" step, which is byte-identical in .github/workflows/devflow.yml, .github/workflows/devflow-implement.yml and .github/workflows/devflow-runner.yml; the #1773 block in lib/test/run.sh.
Architecture Alignment — a new warn block beside the existing guards in the same step, mirroring the shape of the deny guard directly above it. No new file, no new helper, no config key.
Execution tier — the block runs inside a workflow run: step, executed by the Actions runner rather than by the model action. No tool-permission allowlist and no command-shape matcher sits in front of it, so the change adds no grant and cannot be silently denied. The allowlist the workflow does build is assembled in a different step and is untouched.
How consumers get it — the installer already copies these three workflow files, so a consumer picks the change up by re-running the installer. Nothing in install.sh changes.
Dependencies — none beyond what the step already uses (jq).
Data/Schema Considerations — .prflow/config.schema.json describes the provider env map and lists the names the deny guard refuses; the four watched keys are not there yet and the description gains them. The schema's shape does not change.
Cross-layer Impact — three workflow files, one test block, and four documentation surfaces.
Verified:.prflow/config.schema.json states the ordering the whole problem rests on: "The env map is written last in the step, so an accepted key takes effect for every subsequent step in the job, not just the action step."
Setting HOME moves where the model action writes its user settings file, and where gh and git read their configuration in the steps that follow — assumption, confirm before implementing, citing the in-repo comment in .github/workflows/devflow-implement.yml that records reading the action's source and concluding it resolves the user settings path from the home directory.
RUNNER_TEMP is read by name by later steps in all three workflows, among them the git-env pin resolution, the committer identity resolution, the execution-transcript scrub and the observability backstop.
The watched keys are exactly these four — complete by construction: ANTHROPIC_BASE_URL, API_TIMEOUT_MS, HOME, RUNNER_TEMP. When a provider env map names one or more of them, the step prints one ::warning:: naming every key that matched, and exits 0.
A key the env map sets ends up with the map's value, for the watched keys and every other key alike, unchanged from before this issue: an existing provider configuration produces the same GITHUB_ENV contents it produces today, plus the new warning line.
Key matching is case-folded and whole-name, the same rule the deny guard directly above already applies: a map naming home warns exactly as one naming HOME does, and a map naming HOMEDIR produces no warning.
A provider env map naming none of the four watched keys produces no warning, and the step's output is unchanged from before this issue.
Every name the deny guard refuses today still refuses the run with the same ::error::, and the deny guard's list of names is unchanged.
The "Inject provider endpoint" step body is byte-identical across the three workflow files after the change, the property the existing three-way body-identity check asserts.
The test block's per-key refusal loop covers the deny guard's names and none of the four watched keys, so no watched key is asserted to refuse the run.
A provider entry with no env key at all, and one whose env is not an object, behave exactly as they do today — the new warn block does not introduce a new way for the step to fail.
Implementation Notes
Approach — add one warn block to the "Inject provider endpoint" step, beside the guards that already run there. It reads the env map's keys, upper-cases each one and tests it against a four-name set, collects the matches, and prints a single ::warning:: naming them. It reports and continues rather than refusing the run, and it changes neither what is written nor the order it is written in. Every name outside the four is handled by the path that handles it today: the step's final statement exports it verbatim, with no warning. The deny guard, the name-shape guard and the empty-secret guard are all untouched, and no name moves onto the deny list.
The same change narrows how the test block finds the denied names. That extraction currently scans the whole step body for a line containing the membership test, and the new warn block adds a second such line, so the extraction is anchored to the deny guard's own assignment. Without that, the watched keys are swept into the loop that asserts a name refuses the run, and the suite fails on names that are meant to warn.
Relevant files — the three workflow files carrying the step (.github/workflows/devflow.yml, .github/workflows/devflow-implement.yml, .github/workflows/devflow-runner.yml), which must be edited together to stay byte-identical; the #1773 block in lib/test/run.sh for the new assertions; likely .prflow/config.schema.json and docs/external/docs/configuration/providers.md, and plausibly docs/internal/cloud-setup.md and docs/internal/DEVFLOW_SYSTEM_OVERVIEW.md, for the prose copies; and a new .changeset/*.md file.
Code Patterns — copy the deny guard that sits directly above: the same jq shape reading .env // {}, the same ascii_upcase | IN(...) membership test, the same collect-then-report-once form that names every offending key in one message. Keep the command substitution in a bare assignment, exactly as denied_env_keys does and for the same reason the comment beside it gives.
Testing Strategy — extend the #1773 block in lib/test/run.sh, which extracts the real step body and runs it under bash -c against a temporary GITHUB_ENV. That single behavioral level is the whole plan; there is no unit boundary below it and no integration boundary above it.
Start with the bug-reproduction case: feed a map naming ANTHROPIC_BASE_URL and assert no warning is printed. That fails against today's code by exhibiting the reported silence, and passes once the warn block lands.
The warned names are not covered for free. The existing per-key loop derives its population from the deny guard's own list, so it sweeps up denied names automatically and will not touch a warned one. Every case below is hand-written.
Cases to add: one per watched key, asserting the warning names it and the step still exits 0; two keys at once, asserting a single warning names both; a lower-case spelling, asserting it matches; a longer name containing a watched key, asserting it does not; a map naming none of the four, asserting silence; and the Bedrock path with ANTHROPIC_BASE_URL in the map, which is the case where the step's existing "base_url is ignored" warning and the map disagree.
These shape cases guard the new block itself: a provider entry with no env key, and one whose env is not an object. Both must behave as they do today.
governing conventions consulted: CLAUDE.md (the six-shape adversarial matrix for config-JSON consumers), CONTRIBUTING.md, and docs/internal/. The six-shape matrix is deliberately narrowed here to the two shapes above. The justification: the new block reads the same .env value the deny guard already reads, through the same .env // {} guard, so the array, scalar and valid-falsy shapes reach it exactly as they reach the existing guard and are already covered there. Only the absent and non-object shapes exercise the new block's own reading of that value.
Documentation Needed — .prflow/config.schema.json, docs/external/docs/configuration/providers.md, docs/internal/cloud-setup.md, docs/internal/DEVFLOW_SYSTEM_OVERVIEW.md. Each gains the four watched keys and the warn-do-not-refuse rule, kept clearly apart from the deny list so a reader does not take a warned key for a refused one. While editing docs/internal/cloud-setup.md, correct the sentence introducing that bullet: it promises two notes above three bullets, and it says the schema carries none of the content when the schema in fact carries the whole name inventory.
Potential Gotchas
The command substitution must be a bare assignment. Wrapping it in local, declare, or an if/&&/|| context lets set -e swallow a jq failure, and the empty result then reads as "nothing matched" — the warning silently never fires. The deny guard above carries a comment saying exactly this.
The test block derives the denied names by scanning the extracted step body for the membership test and printing every line that matches. A warn block written with the same shape puts the watched keys into that population, and the per-key loop then asserts each one refuses the run with an empty GITHUB_ENV file. They warn and continue, so the suite fails. Anchor the extraction to the deny guard's assignment before adding the block. The extraction already fails closed if it matches nothing at all, because the frozen list of expected names is then reported missing in full.
The three step bodies are asserted byte-identical, comparing the run: bodies only. Editing one file and not the others turns the suite red; the differing SECTION value lives in the step's env: block and is outside that comparison.
The suite cannot check which of two writes wins. Its GITHUB_ENV helpers emit every occurrence of a repeated key rather than resolving a winner, and every assertion over that output is a membership test. So assert the warning fires; do not try to assert precedence.
The four watched names will exist as one executable copy plus several prose copies, and no test reconciles them. This already happened to the deny list, where only the workflow copy is machine-checked, so a name added there keeps the suite green while every prose copy drifts.
Editing files under .github/workflows/ needs a workflows-scoped push.
The covering run for this surface is lib/test/run-shard.sh monolith; there is no focused module.
The change adds a .changeset/*.md file with a patch bump. Nothing starts refusing, so no consumer action is required on upgrade — unlike The provider env map is exported unfiltered and needs a deny list #1773, which shipped as a minor with an upgrade note precisely because it made runs fail.
Generated via /prflow:create-issue (v2.34.2, claude-opus-5, high)
Problem Statement
A maintainer sets up a third-party model provider in
.prflow/config.json. That provider entry has dedicated fields for the endpoint (base_url) and the request timeout (timeout_ms), and a free-formenvmap for anything else. If the maintainer putsANTHROPIC_BASE_URLorAPI_TIMEOUT_MSin theenvmap, it quietly beats the dedicated field, and nothing in the run says so. Two names that are not written by the step at all,HOMEandRUNNER_TEMP, are just as unguarded, and setting one of them moves where every later step in the job looks for its files.This is a config footgun, not a security hole.
.prflow/config.jsonis maintainer-owned and read from a trusted branch, and the two names that would matter for credentials are already refused.Current Behavior
Triggering input. A provider entry whose
envmap names any ofANTHROPIC_BASE_URL,API_TIMEOUT_MS,HOMEorRUNNER_TEMP.Observed result. The "Inject provider endpoint" step exits 0 and prints nothing. It writes its own values first and appends the
envmap last, so the map's value is the one later steps see. On the Bedrock path the run contradicts itself: the step warns thatbase_urlis ignored, while the map puts an endpoint back.Expected result. A warning naming the keys, so the maintainer can see that the
envmap decided the value.Environment. All three cloud workflows, on the GitHub-hosted Actions runner. The defect does not depend on the operating system or the shell.
Desired Behavior
The step keeps working exactly as it does today, and adds one warning. When a provider
envmap names any of the four watched keys, the step prints a single::warning::listing every matched key and carries on. Which value ends up in effect does not change: theenvmap still wins, deliberately, so no existing setup breaks.User Impact
A maintainer who sets one of these keys in the
envmap finds out from the run log instead of from surprising behavior. Everyone else sees no change: a run whoseenvmap names none of the four keys behaves and logs exactly as before.Technical Context
Relevant Classes/Files — the "Inject provider endpoint (provider-routed sections only)" step, which is byte-identical in
.github/workflows/devflow.yml,.github/workflows/devflow-implement.ymland.github/workflows/devflow-runner.yml; the#1773block inlib/test/run.sh.Architecture Alignment — a new warn block beside the existing guards in the same step, mirroring the shape of the deny guard directly above it. No new file, no new helper, no config key.
Execution tier — the block runs inside a workflow
run:step, executed by the Actions runner rather than by the model action. No tool-permission allowlist and no command-shape matcher sits in front of it, so the change adds no grant and cannot be silently denied. The allowlist the workflow does build is assembled in a different step and is untouched.How consumers get it — the installer already copies these three workflow files, so a consumer picks the change up by re-running the installer. Nothing in
install.shchanges.Dependencies — none beyond what the step already uses (
jq).Data/Schema Considerations —
.prflow/config.schema.jsondescribes the providerenvmap and lists the names the deny guard refuses; the four watched keys are not there yet and the description gains them. The schema's shape does not change.Cross-layer Impact — three workflow files, one test block, and four documentation surfaces.
Verified:
.prflow/config.schema.jsonstates the ordering the whole problem rests on: "The env map is written last in the step, so an accepted key takes effect for every subsequent step in the job, not just the action step."Verified:
lib/test/modules/coverage-map.jsonnames the covering test run for this surface: "issue The provider env map is exported unfiltered and needs a deny list #1773 provider env-map key-name deny list; run.sh-resident, driven from the R313_INJ_BODY inject-step extraction beside the Add opt-in third-party model provider support to the cloud tier (per-workflow endpoint, auth, and model selection) #313 assertions (the monolith shard covers them)".Third-party behavior, with its source: the Actions runner stores each
GITHUB_ENVpair by assigning into a dictionary inSetEnvFileCommand, so a later write for the same key replaces an earlier one. Source: https://raw.githubusercontent.com/actions/runner/main/src/Runner.Worker/FileCommandManager.cs — the published documentation at https://docs.github.com/en/actions/reference/workflows-and-actions/variables does not cover repeated writes.Setting
HOMEmoves where the model action writes its user settings file, and whereghandgitread their configuration in the steps that follow — assumption, confirm before implementing, citing the in-repo comment in.github/workflows/devflow-implement.ymlthat records reading the action's source and concluding it resolves the user settings path from the home directory.RUNNER_TEMPis read by name by later steps in all three workflows, among them the git-env pin resolution, the committer identity resolution, the execution-transcript scrub and the observability backstop.Builds on The provider env map is exported unfiltered and needs a deny list #1773, merged, which added the deny guard this change deliberately leaves alone. The provider env deny list omits the names the inject step writes itself #1892 was recorded there as deferred.
Acceptance Criteria
ANTHROPIC_BASE_URL,API_TIMEOUT_MS,HOME,RUNNER_TEMP. When a providerenvmap names one or more of them, the step prints one::warning::naming every key that matched, and exits 0.envmap sets ends up with the map's value, for the watched keys and every other key alike, unchanged from before this issue: an existing provider configuration produces the sameGITHUB_ENVcontents it produces today, plus the new warning line.homewarns exactly as one namingHOMEdoes, and a map namingHOMEDIRproduces no warning.envmap naming none of the four watched keys produces no warning, and the step's output is unchanged from before this issue.::error::, and the deny guard's list of names is unchanged.envkey at all, and one whoseenvis not an object, behave exactly as they do today — the new warn block does not introduce a new way for the step to fail.Implementation Notes
Approach — add one warn block to the "Inject provider endpoint" step, beside the guards that already run there. It reads the
envmap's keys, upper-cases each one and tests it against a four-name set, collects the matches, and prints a single::warning::naming them. It reports and continues rather than refusing the run, and it changes neither what is written nor the order it is written in. Every name outside the four is handled by the path that handles it today: the step's final statement exports it verbatim, with no warning. The deny guard, the name-shape guard and the empty-secret guard are all untouched, and no name moves onto the deny list.The same change narrows how the test block finds the denied names. That extraction currently scans the whole step body for a line containing the membership test, and the new warn block adds a second such line, so the extraction is anchored to the deny guard's own assignment. Without that, the watched keys are swept into the loop that asserts a name refuses the run, and the suite fails on names that are meant to warn.
Relevant files — the three workflow files carrying the step (
.github/workflows/devflow.yml,.github/workflows/devflow-implement.yml,.github/workflows/devflow-runner.yml), which must be edited together to stay byte-identical; the#1773block inlib/test/run.shfor the new assertions; likely.prflow/config.schema.jsonanddocs/external/docs/configuration/providers.md, and plausiblydocs/internal/cloud-setup.mdanddocs/internal/DEVFLOW_SYSTEM_OVERVIEW.md, for the prose copies; and a new.changeset/*.mdfile.Code Patterns — copy the deny guard that sits directly above: the same
jqshape reading.env // {}, the sameascii_upcase | IN(...)membership test, the same collect-then-report-once form that names every offending key in one message. Keep the command substitution in a bare assignment, exactly asdenied_env_keysdoes and for the same reason the comment beside it gives.Testing Strategy — extend the
#1773block inlib/test/run.sh, which extracts the real step body and runs it underbash -cagainst a temporaryGITHUB_ENV. That single behavioral level is the whole plan; there is no unit boundary below it and no integration boundary above it.Start with the bug-reproduction case: feed a map naming
ANTHROPIC_BASE_URLand assert no warning is printed. That fails against today's code by exhibiting the reported silence, and passes once the warn block lands.The warned names are not covered for free. The existing per-key loop derives its population from the deny guard's own list, so it sweeps up denied names automatically and will not touch a warned one. Every case below is hand-written.
Cases to add: one per watched key, asserting the warning names it and the step still exits 0; two keys at once, asserting a single warning names both; a lower-case spelling, asserting it matches; a longer name containing a watched key, asserting it does not; a map naming none of the four, asserting silence; and the Bedrock path with
ANTHROPIC_BASE_URLin the map, which is the case where the step's existing "base_url is ignored" warning and the map disagree.These shape cases guard the new block itself: a provider entry with no
envkey, and one whoseenvis not an object. Both must behave as they do today.governing conventions consulted:
CLAUDE.md(the six-shape adversarial matrix for config-JSON consumers),CONTRIBUTING.md, anddocs/internal/. The six-shape matrix is deliberately narrowed here to the two shapes above. The justification: the new block reads the same.envvalue the deny guard already reads, through the same.env // {}guard, so the array, scalar and valid-falsy shapes reach it exactly as they reach the existing guard and are already covered there. Only the absent and non-object shapes exercise the new block's own reading of that value.Documentation Needed —
.prflow/config.schema.json,docs/external/docs/configuration/providers.md,docs/internal/cloud-setup.md,docs/internal/DEVFLOW_SYSTEM_OVERVIEW.md. Each gains the four watched keys and the warn-do-not-refuse rule, kept clearly apart from the deny list so a reader does not take a warned key for a refused one. While editingdocs/internal/cloud-setup.md, correct the sentence introducing that bullet: it promises two notes above three bullets, and it says the schema carries none of the content when the schema in fact carries the whole name inventory.Potential Gotchas
local,declare, or anif/&&/||context letsset -eswallow ajqfailure, and the empty result then reads as "nothing matched" — the warning silently never fires. The deny guard above carries a comment saying exactly this.GITHUB_ENVfile. They warn and continue, so the suite fails. Anchor the extraction to the deny guard's assignment before adding the block. The extraction already fails closed if it matches nothing at all, because the frozen list of expected names is then reported missing in full.run:bodies only. Editing one file and not the others turns the suite red; the differingSECTIONvalue lives in the step'senv:block and is outside that comparison.GITHUB_ENVhelpers emit every occurrence of a repeated key rather than resolving a winner, and every assertion over that output is a membership test. So assert the warning fires; do not try to assert precedence..github/workflows/needs aworkflows-scoped push.lib/test/run-shard.sh monolith; there is no focused module..changeset/*.mdfile with apatchbump. Nothing starts refusing, so no consumer action is required on upgrade — unlike The provider env map is exported unfiltered and needs a deny list #1773, which shipped as aminorwith an upgrade note precisely because it made runs fail.Generated via /prflow:create-issue (v2.34.2, claude-opus-5, high)