Security gate for AI agent outputs in GitHub Actions. Validates constraints, sanitizes secrets, and applies actions through a controlled write pipeline.
This is an initial implementation inspired by GitHub Next Agentic Workflows (gh-aw).
gh-aw's safe outputs architecture enforces a critical security principle: the AI agent never has direct write access to your repository. Instead, the agent proposes actions as structured data, and a separate gated job validates, sanitizes, and applies them. This action brings that same pattern to standard GitHub Actions workflows where gh-aw's built-in sandbox is not available -- for example, when your agent needs to read from external systems like Kubernetes clusters, Azure resources, or third-party APIs.
gh-aw provides an excellent security model, but its safe outputs are tightly coupled to the gh-aw runtime and cannot be used independently. If your agentic workflow needs to:
- Consume data from external systems (cloud APIs, databases, clusters)
- Run in a standard GitHub Actions environment
- Use a custom agent or model not supported by gh-aw
...then you need to implement your own output gate. This action provides that gate as a reusable, configurable step.
+-----------------------------------------+
| Agent Job (read-only permissions) |
| |
| AI agent analyzes data, produces |
| structured JSON output artifact |
+-----------------------------------------+
|
v (artifact upload/download)
+-----------------------------------------+
| Safe Outputs Job (write permissions) |
| |
| 1. Validate constraints (limits, |
| title prefix, allowed labels) |
| 2. Sanitize secrets (JWTs, keys, |
| connection strings, custom) |
| 3. Apply actions via GitHub API |
+-----------------------------------------+
|
v
+-----------------------------------------+
| GitHub (issues, PRs, comments, labels) |
+-----------------------------------------+
The agent and the write job are separate GitHub Actions jobs with different permission sets. The agent job runs with minimal (ideally read-only) permissions. The write job runs with scoped write permissions but contains no AI reasoning -- it mechanically applies validated, sanitized output.
The agent must produce a JSON file conforming to this schema:
{
"version": "1",
"actions": [
{
"type": "issue_comment",
"issue_number": 42,
"body": "## Analysis Results\n..."
},
{
"type": "create_issue",
"title": "[bot] Node pressure detected",
"body": "Details...",
"labels": ["bug", "auto-generated"],
"assignees": ["octocat"]
},
{
"type": "create_pull_request",
"title": "[bot] Fix HPA configuration",
"body": "This PR adjusts the HPA settings...",
"head": "fix/hpa-config",
"base": "main"
},
{
"type": "add_labels",
"issue_number": 42,
"labels": ["triaged", "cluster-doctor"]
}
]
}| Type | Description | Required fields |
|---|---|---|
issue_comment |
Add a comment to an existing issue or PR | issue_number, body |
create_issue |
Create a new issue | title, body |
create_pull_request |
Create a PR. Optionally provide files to create the branch automatically. |
title, body, head |
add_labels |
Add labels to an existing issue or PR | issue_number, labels |
When files is provided in a create_pull_request action, the safe-outputs
action handles the full workflow:
- Gets the base branch HEAD SHA
- Creates blobs for each file via the Git Data API
- Creates a new tree with those blobs
- Creates a commit on that tree
- Creates (or updates) the branch ref
- Opens the pull request
The agent never needs write access -- all Git operations happen in the gated write job.
{
"type": "create_pull_request",
"title": "[cluster-doctor] Fix HPA configuration",
"body": "Adjusted maxReplicas based on node capacity analysis.",
"head": "fix/hpa-config",
"base": "main",
"files": {
"k8s/hpa.yaml": "apiVersion: autoscaling/v2\nkind: HorizontalPodAutoscaler\nmetadata:\n name: my-app\nspec:\n maxReplicas: 10",
"docs/changes.md": "# Changes\n\nUpdated HPA max replicas to 10."
},
"commit_message": "fix: adjust HPA max replicas based on cluster capacity"
}| Field | Required | Description |
|---|---|---|
files |
No | Map of file path to content. When provided, branch is created automatically. |
commit_message |
No | Commit message for the file commit. Defaults to the PR title. |
File contents are sanitized for secrets just like all other string fields.
jobs:
analyze:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
# Your agent step - produces output JSON
- name: Run AI agent
run: |
# Agent writes its proposed actions to a JSON file
copilot -p "Analyze this repo and propose improvements" \
--output agent-output.json
- uses: actions/upload-artifact@v4
with:
name: agent-output
path: agent-output.json
safe-outputs:
needs: analyze
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/download-artifact@v4
with:
name: agent-output
- uses: microsoftgbb/safe-outputs-action@v0
with:
artifact-path: agent-output.json
max-issues: 1
max-comments: 3
title-prefix: '[bot] '
allowed-labels: 'bug,auto-generated,enhancement'This is the primary use case -- an agent that needs access to external systems:
jobs:
gather-diagnostics:
runs-on: ubuntu-latest
permissions:
id-token: write # Azure OIDC
contents: read
steps:
- uses: azure/login@v2
with:
client-id: ${{ secrets.ARM_CLIENT_ID }}
tenant-id: ${{ secrets.ARM_TENANT_ID }}
subscription-id: ${{ secrets.ARM_SUBSCRIPTION_ID }}
- name: Collect cluster data
run: |
az aks get-credentials --resource-group $RG --name $CLUSTER
kubectl get events -A -o json > diagnostics/events.json
kubectl get pods -A -o json > diagnostics/pods.json
kubectl top nodes -o json > diagnostics/nodes.json
- uses: actions/upload-artifact@v4
with:
name: diagnostics
path: diagnostics/
analyze:
needs: gather-diagnostics
runs-on: ubuntu-latest
permissions:
contents: read # Read-only -- no cloud creds, no write token
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: diagnostics
path: diagnostics/
- name: AI analysis
env:
GITHUB_TOKEN: ${{ secrets.COPILOT_CLI_TOKEN }}
run: |
copilot -p "Analyze the K8s diagnostics in diagnostics/ and produce
a JSON report following the safe-outputs schema. Write to output.json" \
--agent "cluster-doctor"
- uses: actions/upload-artifact@v4
with:
name: agent-output
path: output.json
apply:
needs: analyze
runs-on: ubuntu-latest
permissions:
issues: write
steps:
- uses: actions/download-artifact@v4
with:
name: agent-output
- uses: microsoftgbb/safe-outputs-action@v0
with:
artifact-path: output.json
title-prefix: '[cluster-doctor] '
allowed-labels: 'cluster-doctor,bug,investigation'
custom-secret-patterns: |
10\.0\.\d+\.\d+
aks-[a-z0-9]{8,}Validate and sanitize without applying -- useful for testing:
- uses: microsoftgbb/safe-outputs-action@v0
with:
artifact-path: output.json
dry-run: trueFail the workflow if the agent output contains any sensitive data, instead of redacting and proceeding:
- uses: microsoftgbb/safe-outputs-action@v0
with:
artifact-path: output.json
fail-on-sanitize: true| Input | Description | Required | Default |
|---|---|---|---|
artifact-path |
Path to the agent output JSON file | Yes | - |
max-issues |
Max issues the agent can create per run | No | 1 |
max-comments |
Max comments the agent can create per run | No | 3 |
max-pull-requests |
Max PRs the agent can create per run | No | 1 |
max-labels |
Max add-labels actions per run | No | 5 |
title-prefix |
Required prefix for issue/PR titles | No | '' |
allowed-labels |
Comma-separated label allowlist (empty = all allowed) | No | '' |
custom-secret-patterns |
Additional regex patterns, one per line | No | '' |
dry-run |
Validate and sanitize without applying | No | false |
fail-on-sanitize |
Fail if any content is redacted | No | false |
token |
GitHub token for write operations | No | ${{ github.token }} |
| Output | Description |
|---|---|
applied-count |
Number of actions successfully applied |
blocked-count |
Number of actions blocked by constraints |
sanitized-count |
Number of fields with redacted content |
summary |
JSON summary of all phases |
The sanitizer scans for these patterns by default:
- JWTs --
eyJ...header.payload.signature format - Azure connection strings --
DefaultEndpointsProtocol=... - Azure SAS tokens -- URL query parameters with signature components
- AWS access keys --
AKIA...,ASIA...prefixes - GitHub tokens --
ghp_,gho_,ghs_,ghu_,ghr_prefixes - Private key blocks -- PEM-encoded private keys
- Bearer tokens --
Bearer <token>patterns - Generic key=value secrets --
password=,secret:,api_key=, etc. - Hex-encoded tokens -- Long hex strings associated with secret-like key names
Add domain-specific patterns (e.g., internal IPs, cluster names) via the
custom-secret-patterns input.
When threat-detection: true is set and the Copilot CLI is installed on the
runner, the action runs an AI-powered scan of the agent's proposed output.
This catches threats that regex-based sanitization misses:
- Prompt injection -- content designed to manipulate downstream LLMs
- Encoded credentials -- base64-wrapped secrets, obfuscated tokens
- Malicious code -- backdoors, data exfiltration, CI/CD weakening
- Social engineering -- issue/PR content designed to trick human reviewers
- uses: microsoftgbb/safe-outputs-action@v0
with:
artifact-path: output.json
threat-detection: true
env:
GITHUB_TOKEN: ${{ secrets.COPILOT_CLI_TOKEN }}If the Copilot CLI is not available on the runner, threat detection is silently skipped and the action proceeds with regex-based sanitization only.
The pipeline with threat detection enabled:
Validate constraints -> Sanitize secrets -> AI threat scan -> Apply actions
This action provides the output gate: it validates constraints, sanitizes secrets, runs optional AI threat detection, and applies the agent's proposed actions through a controlled write pipeline. However, it does not control what the agent can access while it runs (network, filesystem, environment).
For full defense-in-depth, pair it with agent-sandbox-action, which provides input containment: the agent runs inside a Docker container on an isolated network with a Squid proxy enforcing a domain allowlist.
Together the two actions cover both halves of the security model:
| Layer | Action | What it guards |
|---|---|---|
| Input containment | agent-sandbox-action |
Network access, filesystem, environment |
| Output gate | safe-outputs-action |
Comments, PRs, file writes, secret leakage |
# Full defense-in-depth pipeline for AI agents
# Combines agent-sandbox-action (input containment) with
# safe-outputs-action (output gate)
jobs:
diagnose:
runs-on: ubuntu-latest
permissions:
contents: read
env:
AGENT_ALLOWED_DOMAINS: |
api.githubcopilot.com
api.github.com
.azmk8s.io
login.microsoftonline.com
steps:
- uses: actions/checkout@v5
- uses: microsoftgbb/agent-sandbox-action@v1
with:
command: |
copilot -p "Analyze the cluster diagnostics and produce
findings as agent-output.json following the safe-outputs
schema" --agent cluster-doctor --allow-all-tools
allowed-domains: ${{ env.AGENT_ALLOWED_DOMAINS }}
env-vars: |
GITHUB_TOKEN=${{ secrets.COPILOT_CLI_TOKEN }}
extra-mounts: |
${{ env.HOME }}/.kube/config:/home/agent/.kube/config:ro
- uses: actions/upload-artifact@v4
with:
name: agent-output
path: agent-output.json
scan:
needs: diagnose
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/download-artifact@v4
with:
name: agent-output
- uses: microsoftgbb/safe-outputs-action@v1
with:
artifact-path: agent-output.json
max-comments: 2
max-pull-requests: 1
title-prefix: "[bot] "
threat-detection: true
dry-run: true
- uses: actions/upload-artifact@v4
with:
name: scanned-output
path: agent-output.json
overwrite: true
apply:
needs: [diagnose, scan]
runs-on: ubuntu-latest
permissions:
issues: write
contents: write
pull-requests: write
steps:
- uses: actions/download-artifact@v4
with:
name: scanned-output
- uses: microsoftgbb/safe-outputs-action@v1
with:
artifact-path: agent-output.json
max-comments: 2
max-pull-requests: 1
title-prefix: "[bot] "Job 1 - diagnose: Runs the AI agent inside the sandbox with network access
limited to the domain allowlist. The agent produces agent-output.json.
Job 2 - scan: Downloads the artifact and runs safe-outputs-action in
dry-run mode. This validates constraints, sanitizes secrets, and (optionally)
runs AI threat detection - without writing anything.
Job 3 - apply: If the scan passes, the validated artifact is applied: the action creates comments, PRs, or file changes on behalf of the agent.
| Dimension | gh-aw safe outputs | This action |
|---|---|---|
| Integration | Built into gh-aw runtime | Standalone GitHub Action |
| Agent scope | Repo-scoped only | Any data source |
| Threat detection | AI-powered scan | Regex-based sanitization |
| Configuration | Markdown frontmatter | Action inputs |
| Network firewall | Built-in (AWF) | Not included (use separately) |
| Customization | Declarative constraints | Full control via inputs |
This action implements the output gate portion of gh-aw's security model. For the full defense-in-depth stack, combine it with:
- Scoped RBAC on your cloud credentials (read-only K8s ClusterRole, Azure Reader)
- Network firewall via container networking or gh-aw's AWF
- Separate jobs with distinct permission sets (gather / analyze / apply)
npm install
npm test
npm run buildTo verify the dist is up to date:
npm run build
git diff dist/MIT