A model can write a valid GitHub Actions workflow that still leaks a secret. Lint the syntax, then gate the permissions.
The failure mode
Most checks on model-generated YAML stop at syntax. A workflow can parse cleanly, pass actionlint, and still:
- request
id-token: writeorcontents: write - read repository secrets in an unexpected step
- add a job that pushes to the default branch
- run on
pull_request_targetwith elevated permissions
If the YAML came from a prompt, treat it as a patch, not a trusted config.
When a workflow is drafted by MonkeyCode’s free model, nobody should review the whole file line by line. Run a small permission gate instead. Disclosure: This article was prepared as part of MonkeyCode's product outreach.
The artifact: a read-only gate
The gate has two layers:
-
actionlintcatches malformed expressions and unsupported keys. - A small Python check rejects workflows that ask for write permissions or touch secrets.
Save this as check_workflow.py:
import sys
import yaml
def main(path):
with open(path) as f:
data = yaml.safe_load(f)
perms = data.get("permissions", {})
if perms.get("contents") != "read":
print("workflow-level permissions.contents must be read", file=sys.stderr)
return 1
for name, job in data.get("jobs", {}).items():
job_perms = job.get("permissions", {})
if job_perms.get("contents") == "write":
print(f"job {name} requests contents: write", file=sys.stderr)
return 1
if any("secrets" in str(step) for step in job.get("steps", [])):
print(f"job {name} references secrets", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1]))
Enter fullscreen mode Exit fullscreen mode
Run it with:
actionlint .github/workflows/draft.yml
python check_workflow.py .github/workflows/draft.yml
Enter fullscreen mode Exit fullscreen mode
This is not a complete security review. The secrets check is intentionally coarse: it flags any step whose string form contains the word secrets. A real gate should parse the workflow with a schema or an AST, but this catches the common mistake before merge.
What the gate catches
Consider this model-generated fragment:
name: Draft
on: [pull_request_target]
permissions: write-all
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: echo "${{ secrets.DEPLOY_KEY }}" | base64
Enter fullscreen mode Exit fullscreen mode
actionlint will probably accept the file. The Python gate rejects it immediately: workflow-level permissions is not contents: read, and a step mentions secrets. That is the common failure mode: valid syntax, invalid trust.
Why a free server helps
The MonkeyCode free server option gives a clean place to run these checks without installing actionlint and PyYAML on a local machine every time.
The flow stays small:
- generate the YAML with the free model
- write it to a draft file
- run the two checks in the free server shell
- merge only if the checks return zero
The draft can stay isolated from local secrets. The free server sees only the draft file, not repository tokens.
What this gate does not catch
Static checks cannot prove intent. A workflow can pass the gate and still:
- exfiltrate data over a network at runtime
- abuse a third-party action with unexpected behavior
- hide malicious code inside a shell step that only writes read-only outputs
- use
pull_request_targetto read secrets indirectly
The permission check only looks at declared YAML. It cannot know what a step’s command actually does. actionlint validates syntax, not trust.
Who should not rely on this
- Repositories that manage production secrets or deploy artifacts.
- Teams that need write permissions for normal CI jobs.
- Workflows that use reusable workflows or dynamic permissions the simple checker cannot parse.
- Anyone unwilling to review the final diff before merge.
For those cases, the gate needs to be stricter: require signed commits, pin actions by SHA, and run the workflow in an isolated environment first.
A smaller default
The fastest improvement is to make permissions: contents: read the default for every model-generated workflow. Most free-model drafts do not need write access. If the prompt asks for a CI step that only runs tests, the workflow should not be able to push.
Start from read-only. Reject write requests. Add back the minimum permission only after a human reads the diff.
A valid YAML file is not a valid action. Gate the permissions before you trust the model’s output.