GitHub Actions and secrets: the CI/CD mistakes vibe coders make
By Seaworthy · 25 June 2026
Photo by Pankaj Patel on Unsplash
AI writes GitHub Actions workflows fluently. It handles YAML syntax and action references without hesitation, sets up build matrices without complaint. What it consistently gets wrong is secrets handling, and the errors it makes are the kind that do not fail loudly. They fail silently, after your API key has already been exfiltrated.
Where the secrets go
GitHub Actions has a secrets mechanism that looks safe. You store a value in the repository settings, reference it as ${{ secrets.MY_KEY }}, and GitHub masks it in logs. The problem is that masking only works for exact string matches in log output. There are several ways to bypass it without trying.
The most common mistake in AI-generated workflows is passing a secret as an environment variable and then doing something with it that expands or transforms the value.
jobs:
deploy:
runs-on: ubuntu-latest
env:
API_KEY: ${{ secrets.API_KEY }}
steps:
- name: Deploy
run: |
echo "Deploying with key: ${API_KEY:0:8}..."
curl -H "Authorization: Bearer $API_KEY" https://api.example.com/deploy
The slice ${API_KEY:0:8}... prints the first eight characters of your secret. GitHub's masker does not know to mask that substring. It is now in your build logs, readable by anyone with repository access, or by anyone at all if the repo is public.
The curl command is also a risk. If the request fails and curl prints its full command line, the header value lands in the log.
The pull_request_target trap
This one is serious, and AI-generated workflows reproduce it constantly.
pull_request_target was introduced to let workflows comment on PRs from forks. Unlike pull_request, it runs in the context of the base repository, which means it has access to repository secrets. AI assistants often suggest it when you want to add a bot comment or run checks against external PRs, without explaining the trade-off.
on:
pull_request_target:
types: [opened, synchronize]
jobs:
comment:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }} # checks out attacker code
- run: npm install && npm test # runs attacker code with your secrets
That ref line is the problem. You are checking out code from the fork, then executing it in a context that has access to your secrets. An attacker opens a PR, modifies package.json to run a script that exfiltrates secrets.DEPLOY_TOKEN, and your workflow hands it to them.
The fix: never check out or run code from the PR head in a pull_request_target workflow. If you need to comment on a fork PR, do it in a separate job with no access to secrets, triggered by pull_request instead.
Third-party actions are code you run
Every uses: line in your workflow is an instruction to download and execute someone else's code. AI-generated workflows reference third-party actions casually, often without pinning them to a specific commit SHA.
# What AI generates
- uses: some-org/some-action@main
# What you should do
- uses: some-org/some-action@a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2
The @main reference means the action can change between runs. The maintainer, or an attacker who compromises the maintainer's account, can push new code that reads your secrets and exfiltrates them. Using a commit SHA pins you to a specific, auditable version. Yes, it is more annoying to update. That is the point.
Artifacts and environment dumps
Another pattern in AI-generated workflows is uploading debug artifacts that contain environment variable dumps.
- name: Debug environment
if: failure()
run: env > debug.txt
- uses: actions/upload-artifact@v4
if: failure()
with:
name: debug-info
path: debug.txt
env prints everything, including any secrets you have mapped into environment variables. That artifact is now downloadable by anyone with read access to the repository. The if: failure() condition makes it feel like a defensive measure. It is not.
A safer structure
Pin third-party actions to commit SHAs. Use environment-scoped secrets rather than repository-level ones, so each job only has access to what it actually needs. Never pass secrets into env: without checking what that step does with them.
Splitting build and deploy into separate jobs is the most effective structural change:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- run: npm ci && npm run build
# No secrets here
deploy:
needs: build
runs-on: ubuntu-latest
environment: production # environment-scoped secrets
if: github.event_name != 'pull_request'
steps:
- run: ./scripts/deploy.sh
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
The deploy job only runs on non-PR events, which keeps your secrets away from attacker-controlled code. The build job can receive untrusted input without any access to credentials.
For pull_request_target, audit every single workflow that uses it. If it checks out PR code, it is almost certainly wrong.
Seaworthy scans your repository for these patterns, including pull_request_target misuse, unpinned third-party actions, and secrets mapped into environment variables in positions where they can leak.
This article was generated by AI and summarises publicly available sources.
Seaworthy scans your repo for the issues covered in articles like this one.
Security gaps, exposed secrets, and misconfigurations — caught before you deploy. Free to run, no account needed.