Skip to content

Against PromptPwnd and Comment and Control

← Security design

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:

The pattern that PromptPwnd attacks, and the assistant's: In the attacked pattern, the text of an issue goes into the prompt of an AI that holds a write token, a shell and the network, so a steered AI writes to GitHub or sends secrets away. In the assistant, the text of the issue goes into files that the prompt calls material; the AI reads them with a read-only token and reading tools alone, behind a firewall, and answers with JSON. A job without AI and without the engine's secret checks that answer and is the only one that writes to GitHub.The pattern that PromptPwnd attacks, and the assistant's: In the attacked pattern, the text of an issue goes into the prompt of an AI that holds a write token, a shell and the network, so a steered AI writes to GitHub or sends secrets away. In the assistant, the text of the issue goes into files that the prompt calls material; the AI reads them with a read-only token and reading tools alone, behind a firewall, and answers with JSON. A job without AI and without the engine's secret checks that answer and is the only one that writes to GitHub.

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.