Environment
- Compiler version:
v0.71.5
- Central repo dispatching agents to multiple target repos
- Dynamic
target_repo passed as workflow input
Steps to Reproduce
- Create a workflow
.md with allowed-repos using a ${{ inputs.target_repo }} expression (see frontmatter below)
- Run
gh aw compile
- Trigger the workflow with a
target_repo input pointing to a different repo than the one hosting the workflow
- Observe the
Process Safe Outputs step in the safe_outputs job
Problem Statement
When allowed-repos in the .md frontmatter uses a ${{ }} expression,
the compiled config.json receives the literal string
${{ inputs.target_repo }} instead of the resolved value.
The handler then cannot match the actual repo name and falls back to the
source repo, causing all cross-repo PRs to fail validation.
Root Cause
The compiler generates the Write Safe Outputs Config step in the agent
job using a single-quoted heredoc:
cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << 'GH_AW_SAFE_OUTPUTS_CONFIG_EOF'
{"create_pull_request":{"allowed_repos":["${{ inputs.target_repo }}"],...}}
GH_AW_SAFE_OUTPUTS_CONFIG_EOF
Single-quoted heredoc delimiters in bash suppress all expansion.
So config.json is written with ${{ inputs.target_repo }} as a literal string.
GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG (a step-level env var) correctly resolves
the expression via the Actions engine — but safe_output_handler_manager.cjs
reads validation config from config.json (downloaded via artifact),
not from the env var.
.md Frontmatter
safe-outputs:
github-app:
app-id: "1234567"
private-key: ${{ secrets.GH_AW_DA_AUTHOR_PRIVATE_KEY }}
create-pull-request:
allowed-repos:
- ${{ inputs.target_repo }}
allowed-base-branches:
- ${{ inputs.base_branch }}
title-prefix: "[ai] "
labels: [automation, my-agent]
draft: false
max: 1
fallback-as-issue: false
Evidence: Config Loads Correctly in Logs
GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG (env var — step level, resolved by Actions engine)
shows the correct resolved value:
{
"create_pull_request": {
"allowed_repos": ["MY-Org/MYREPO"],
"allowed_base_branches": ["main"]
}
}
⚠️ Logs sanitized — private repository. Repo names replaced with MY-Org/MYREPO
and MY-Org/Source-Repo as placeholders. Behavior is reproducible with any
workflow using a dynamic allowed-repos expression targeting a different repo
than the one hosting the workflow.
Evidence: Validation Then Fails
ERR_VALIDATION: Repository 'MY-Org/MYREPO' is not in the allowed-repos list.
Allowed: MY-Org/Source-Repo
MY-Org/Source-Repo is github.repository — the running workflow's own repo.
This confirms the handler ignored GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG and
read from config.json which contains an unresolved literal.
⚠️ Logs sanitized — private repository. Repo names replaced with MY-Org/MYREPO
and MY-Org/Source-Repo as placeholders. Behaviour is reproducible with any
workflow using a dynamic allowed-repos expression targeting a different repo
than the one hosting the workflow.
Failing Run
Workflow runs in a private enterprise repository and cannot be shared publicly.
The log evidence below has been sanitized with placeholder values.
Impact
- Any workflow using a dynamic
allowed-repos value is broken
- Cross-repo agent deployments are not functional without a workaround
- Affects all orgs running centralized repo patterns with multiple target repos
- Blocks legitimate use cases: multiple target repos, multi-environment (dev/prod),
100s of agents that cannot have repo names hardcoded
Expected Behaviour
config.json should contain the resolved value MY-Org/MYREPO,
matching what GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG correctly shows.
Possible Fixes
Option A — Compiler fix: Write config.json by setting env vars first,
then use an unquoted heredoc so the shell expands them at runtime:
# Actions expression engine resolves this before the step runs
export GH_AW_ALLOWED_REPO="${{ inputs.target_repo }}"
export GH_AW_ALLOWED_BRANCH="${{ inputs.base_branch }}"
# Unquoted delimiter — bash now expands ${GH_AW_ALLOWED_REPO}
cat > "${RUNNER_TEMP}/gh-aw/safeoutputs/config.json" << GH_AW_SAFE_OUTPUTS_CONFIG_EOF
{"create_pull_request":{"allowed_repos":["${GH_AW_ALLOWED_REPO}"],"allowed_base_branches":["${GH_AW_ALLOWED_BRANCH}"],...}}
GH_AW_SAFE_OUTPUTS_CONFIG_EOF
Option B — Handler fix: Have safe_output_handler_manager.cjs prefer
GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG over config.json for validation,
since the env var is correctly resolved by the Actions expression engine
and is already present in the same step.
Workarounds (both are not viable at scale)
- Hardcode repo names in
.md frontmatter — breaks multi-repo and multi-environment setups
- Manually edit
.lock.yml — overwritten every time gh aw compile is run
Environment
v0.71.5target_repopassed as workflow inputSteps to Reproduce
.mdwithallowed-reposusing a${{ inputs.target_repo }}expression (see frontmatter below)gh aw compiletarget_repoinput pointing to a different repo than the one hosting the workflowProcess Safe Outputsstep in thesafe_outputsjobProblem Statement
When
allowed-reposin the.mdfrontmatter uses a${{ }}expression,the compiled
config.jsonreceives the literal string${{ inputs.target_repo }}instead of the resolved value.The handler then cannot match the actual repo name and falls back to the
source repo, causing all cross-repo PRs to fail validation.
Root Cause
The compiler generates the
Write Safe Outputs Configstep in theagentjob using a single-quoted heredoc:
Single-quoted heredoc delimiters in bash suppress all expansion.
So
config.jsonis written with${{ inputs.target_repo }}as a literal string.GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG(a step-level env var) correctly resolvesthe expression via the Actions engine — but
safe_output_handler_manager.cjsreads validation config from
config.json(downloaded via artifact),not from the env var.
.md Frontmatter
Evidence: Config Loads Correctly in Logs
GH_AW_SAFE_OUTPUTS_HANDLER_CONFIG(env var — step level, resolved by Actions engine)shows the correct resolved value:
{ "create_pull_request": { "allowed_repos": ["MY-Org/MYREPO"], "allowed_base_branches": ["main"] } }Evidence: Validation Then Fails
MY-Org/Source-Repoisgithub.repository— the running workflow's own repo.This confirms the handler ignored
GH_AW_SAFE_OUTPUTS_HANDLER_CONFIGandread from
config.jsonwhich contains an unresolved literal.Failing Run
Workflow runs in a private enterprise repository and cannot be shared publicly.
The log evidence below has been sanitized with placeholder values.
Impact
allowed-reposvalue is broken100s of agents that cannot have repo names hardcoded
Expected Behaviour
config.jsonshould contain the resolved valueMY-Org/MYREPO,matching what
GH_AW_SAFE_OUTPUTS_HANDLER_CONFIGcorrectly shows.Possible Fixes
Option A — Compiler fix: Write
config.jsonby setting env vars first,then use an unquoted heredoc so the shell expands them at runtime:
Option B — Handler fix: Have
safe_output_handler_manager.cjspreferGH_AW_SAFE_OUTPUTS_HANDLER_CONFIGoverconfig.jsonfor validation,since the env var is correctly resolved by the Actions expression engine
and is already present in the same step.
Workarounds (both are not viable at scale)
.mdfrontmatter — breaks multi-repo and multi-environment setups.lock.yml— overwritten every timegh aw compileis run