Against PromptPwnd and Comment and Control¶
Two published attacks show what goes wrong when an AI in GitHub Actions reads text that anyone can write:
- PromptPwnd, published by Aikido Security in December 2025: the text of an issue or a pull request lands in the prompt of an AI agent that holds a strong token, and makes it run commands or write secrets into the issue.
- Comment and Control, published in April 2026, as SecurityWeek reports: one pull request or comment made Claude Code Security Review, the Gemini CLI action and the GitHub Copilot agent leak their secrets into public logs or comments.
The research note of the Cloud Security Alliance of May 2026 describes both and lists what protects a workflow. The difference to the assistant in one picture:
Each mitigation that the three sources recommend, how the assistant implements it, and the test that pins it:
| Mitigation | Recommended by | In the assistant | Pinned by |
|---|---|---|---|
| Give the agent no tool that writes to issues or pull requests, and no shell | Aikido, CSA | Each engine gets only tools that read files of the checkout, as the boundary of each engine shows: Claude Code runs with --restricted and the tools Read, Grep and Glob alone, Codex runs its commands in a sandbox that can neither write nor reach the network, and the Gemini CLI and the Copilot CLI get their reading tools alone. Their token only reads, or they hold none |
test_each_engine_can_only_read_the_checkout, test_only_the_analyze_job_runs_an_engine_and_only_with_read_permissions in tests/test_templates.py |
| Keep untrusted text out of the prompt, or clearly apart from its instructions | Aikido, CSA | The rules come before the task, which names files: as the system prompt of Claude, the developer instructions of Codex, or the top of the prompt of the Gemini CLI and the Copilot CLI, which take no system prompt. The issue, its comments, the other issues and the discussions reach the AI in them, and the rules call them material to analyze, never instructions. In issue.md and discussion.md, each text stands in a code fence that it can't close, under a heading that only the script writes, and no text keeps a character that GitHub shows as nothing. No expression of their text stands in a script of a workflow |
test_the_prompt_keeps_the_text_of_issues_apart_from_its_instructions, test_no_text_forms_a_heading_or_a_comment_in_issue_md, test_no_invisible_character_reaches_the_engine in tests/test_assistant.py; test_untrusted_text_never_reaches_a_shell_script and test_each_engine_can_only_read_the_checkout, which pins the system prompt, in tests/test_templates.py |
| Treat the output of the AI as untrusted | Aikido | apply checks the answer against its schema, the labels and the issues that exist, refuses it when it holds anything like a secret, and cleans every text of mentions, images, HTML and foreign links |
test_an_answer_that_breaks_the_schema_is_refused, test_any_answer_is_checked_or_refused, test_tricky_text_is_cleaned_safely in tests/test_assistant.py; tests/test_properties.py; fuzz/fuzz_sanitize.py, fuzz/fuzz_render.py |
| Keep the AI apart from the credentials that write, and write only after its output is checked | CSA | analyze holds the engine's secret and a token that only reads. apply holds the token that writes, runs no AI, never sees the engine's secret and starts only after analyze |
test_the_answer_reaches_github_only_through_the_checks, test_only_the_analyze_job_runs_an_engine_and_only_with_read_permissions |
| Limit what a leaked token can reach: Aikido suggests GitHub's IP allow lists, CSA monitoring on the runner | Aikido, CSA | The AI's own token only reads and ends with its job, and the AI runs behind GitHub's egress firewall with an allow list. Every job with a token that writes starts with Harden-Runner, which blocks every host but GitHub's | test_the_firewall_template_allows_only_named_hosts, test_jobs_without_the_ai_reach_only_github |
| Keep secrets out of public logs and comments | Comment and Control, CSA | The AI reads no file outside the checkout, and only Codex has a shell, in a sandbox that can't reach the network, while the action's proxy holds its key out of reach; the Gemini CLI and the Copilot CLI get no variable of the job but their own secret, so their environment holds no token of the job. Copilot's secret is itself a token of GitHub, a fine-grained one, which reaches only what its owner granted: create it with the permission Copilot Requests alone and no access to repositories. An answer with anything like a token is refused as a whole, and no answer can run a workflow command | test_a_token_anywhere_in_the_answer_is_refused, test_no_text_of_the_engine_runs_a_workflow_command |
| Explicit, least permissions in every workflow | CSA | Each template and each reusable workflow that it calls starts with permissions: {}, each job names the permissions it needs, and the job of a template that calls the jobs gives them exactly what they need together; no job may ask for an OIDC token |
test_templates_start_without_permissions_and_not_in_forks, test_each_template_calls_its_reusable_workflow_with_what_its_jobs_need |
| Pin every action to a full commit hash | CSA | Every action of the templates and of the reusable workflows is pinned, the templates pin the reusable workflows of a release, and check keeps each repository's workflows equal to the templates |
test_actions_are_pinned_and_checkouts_keep_no_credentials |
No pull_request_target that runs code of the pull request |
CSA | The lifecycle starts on pull_request_target only to learn which issues a merge fixes; it checks out the default branch and runs no code of the pull request |
test_merges_and_releases_start_the_lifecycle_without_running_pull_request_code |
| Keys for the AI that are scoped to their environment | CSA | The engine's secret lives in the environment issue-assistant, which only the default branch may use, and only analyze gets it. How long the key lives is the choice of whoever creates it |
test_only_the_analyze_job_runs_an_engine_and_only_with_read_permissions, test_only_the_job_of_the_ai_gets_the_engine_s_secrets |
| Attack the workflow with prompt injection before it ships | CSA | Steered answers are tests: crafted examples, and property tests and fuzzing of the cleaned text and of whole comments, on every change and every week, judged by a model of GitHub's rendering of their own | test_tricky_text_is_cleaned_safely; tests/test_properties.py; tests/test_oracle.py; fuzz/fuzz_sanitize.py, fuzz/fuzz_render.py |
Two of the recommendations of the Cloud Security Alliance have nothing to apply to here: the templates sign in to no cloud, so there is no OIDC trust policy to narrow, and the assistant keeps no credential file of an AI tool in a repository.