Skip to content

The engines and their packages

← Security design

The boundary of each engine

Each engine runs in one step of the job analyze, on the runner with GitHub's egress firewall, which lets it reach only the hosts of its own block in .github/egress-firewall.yaml; install writes the block of the engine of config.toml alone. On the runner that ISSUE_ASSISTANT_AI_RUNNER names in place of the firewall runner, Harden-Runner blocks every other host for the job, with the hosts of the same file, and the job stops when requests to a host outside them still get through after a minute, as decision 0019 explains. What keeps it to reading differs:

Engine Where the boundary lies What it can read What it holds
Claude The Claude Code action: --restricted, which removes every tool that runs code or reaches the network and ignores settings files, and --tools "Read,Grep,Glob" The checkout, but not the issue assistant's own checkout in the context folder The job's token, which only reads, and the engine's secret
Codex The Codex action: the permission profile :read-only, whose sandbox lets the commands of Codex read files but neither write them nor reach the network; drop-sudo, after which Codex can't read the memory of the proxy that holds the API key; web search off Any file that the runner's user may read The engine's secret, through the action's proxy alone
Gemini The analyze command: system settings of the Gemini CLI, which come before those of the user and of the repository's .gemini/settings.json, with the tools read_file, list_directory, glob and grep_search alone, and no MCP server, extension, agent, skill, hook, tool that a command finds, telemetry or checkpoint Its workspace, the checkout The engine's secret alone: no token of GitHub, no other variable of the job
Copilot The analyze command: --available-tools view,glob,grep, the only tools the model gets, without GitHub's built-in MCP server and the temporary folder The checkout, its working folder The engine's secret alone, no token of the job and no other variable of it. The secret is a fine-grained token of GitHub; with the permission Copilot Requests alone and no access to repositories, it reaches nothing but Copilot

analyze installs the Gemini CLI or the Copilot CLI from its lock file in engines/ with npm ci, without the scripts of its packages, as the packages of the engines shows, and runs it in a home of its own outside the checkout. The Gemini CLI takes system settings only when root owns them and every folder around them and nobody else may write to them; otherwise it skips them with a warning and runs without them. So analyze writes them with sudo into a folder of the run below /etc/issue-assistant, removes it after the run, and stops without sudo rather than run Gemini without them. test_each_engine_can_only_read_the_checkout in tests/test_templates.py pins every row: the inputs of each action, the arguments and settings of each command line, and that no step passes an engine more.

Those tests pin our side of each boundary. The other side, the names that the vendor's action or command line takes, changes with its release, and a renamed one fails nowhere: GitHub only warns about an input that an action doesn't have, and the Gemini CLI and Codex ignore a key that they don't know. So the check engine contracts of the Action test checks the names against the pinned versions, without a secret, on every pull request that changes an engine, such as an update of Renovate, and every week, as the architecture describes:

  • Claude: every input that the step passes to the Claude Code action at its pin, and the outputs structured_output and execution_file. Its flags in claude_args run with the secret in the Canary.
  • Codex: every input of the Codex action at its pin, among them permission-profile, safety-strategy and allow-users, its output final-message, and that the action still selects the profile through default_permissions. Codex at the version of codex-version takes --json and starts its turn with web_search, developer_instructions and default_permissions under --strict-config, which fails on a key that it doesn't know.
  • Gemini: every flag in the --help of the pinned version, with its value, and every key of its system settings, such as tools.core, tools.exclude, admin.mcp and experimental.enableAgents, in the JSON schema of the settings of that version. Run without a key in a folder like that of analyze, it stops at the key alone and neither refuses its settings nor skips them. Run once more with broken settings in the file that GEMINI_CLI_SYSTEM_SETTINGS_PATH names, placed as analyze places them, it must refuse them naming that file: a release that no longer reads the variable, or skips the file, would otherwise run without its settings and say nothing.
  • Copilot: every flag in the --help of the pinned version, such as --available-tools, --allow-tool, --disable-builtin-mcps and --disallow-temp-dir, with each value that its step gives it, among them each level of effort; run without a key, it stops at the key alone.
  • The lock files of Gemini and Copilot: installed as analyze installs them on GitHub's Ubuntu x64 runner, every package that npm installs there, which is every package that analyze installs, has a signature of npm's registry that verifies, and an attestation that verifies where it has one, and npm refuses a copy of the lock file with another hash of the command line.

The packages of the engines

The Gemini CLI and the Copilot CLI come from npm, with the packages that they depend on, and run in the job that holds the engine's secret. So analyze installs only packages that a check has verified, byte for byte:

How the packages of Gemini and Copilot reach the job analyze: Renovate updates the lock file of the Gemini CLI or the Copilot CLI in engines/, the command line a week after its release and the other packages every Monday, each package at one version with its SHA-512 hash. The check engine contracts of the pull request installs the lock file as analyze does, verifies the signature of npm's registry of every package and the attestations of those that have them, and makes sure that npm refuses another hash; a failure keeps the update from merging. A patch version merges on its own once every required check passed, a minor or major version and the refresh of the other packages when the maintainer merges them, and a release brings it to every repository. In the job analyze, the checkout of the issue assistant at the commit of the workflow holds the lock file, and npm ci installs it in a folder of the run, without the scripts of the packages and without the engine's secret, from npm's registry alone. A package whose hash isn't the one of the lock file stops the analysis before anything of it runs; otherwise the command line runs with the engine's secret.How the packages of Gemini and Copilot reach the job analyze: Renovate updates the lock file of the Gemini CLI or the Copilot CLI in engines/, the command line a week after its release and the other packages every Monday, each package at one version with its SHA-512 hash. The check engine contracts of the pull request installs the lock file as analyze does, verifies the signature of npm's registry of every package and the attestations of those that have them, and makes sure that npm refuses another hash; a failure keeps the update from merging. A patch version merges on its own once every required check passed, a minor or major version and the refresh of the other packages when the maintainer merges them, and a release brings it to every repository. In the job analyze, the checkout of the issue assistant at the commit of the workflow holds the lock file, and npm ci installs it in a folder of the run, without the scripts of the packages and without the engine's secret, from npm's registry alone. A package whose hash isn't the one of the lock file stops the analysis before anything of it runs; otherwise the command line runs with the engine's secret.

  • A lock file for each. engines/gemini/ and engines/copilot/ each hold a package.json that pins the command line, and its package-lock.json, which names every package that it installs, the optional ones for other systems too, at one version, from npm's registry, with its SHA-512 hash. test_the_lock_file_pins_every_package_with_its_hash in tests/test_engines.py keeps them so, and the checkout of the issue assistant in each job holds them at the commit of the workflow.
  • npm ci, and nothing that npm resolves. analyze copies the folder of its engine into a folder of the run and runs npm ci --ignore-scripts there. npm installs each package at the version of the lock file, refuses one whose hash isn't the one of the lock file, and fails when the lock file doesn't match its package.json. It runs no script of a package, reads no .npmrc of the checkout, and sees neither the engine's secret nor a token of GitHub. A package that the registry sends with another hash stops the analysis, as EINTEGRITY, before anything of it runs.
  • Signatures, verified where the lock file changes. Renovate updates the lock files, the command line a week after its release and the other packages every Monday, and the refresh of the other packages and a minor or major version of the command line wait for the maintainer, while a patch version merges on its own once every required check passed (decision 0020). The required check engine contracts of its pull request installs the lock file as analyze does, runs npm audit signatures, which verifies the signature of npm's registry of every package that npm installed on GitHub's Ubuntu x64 runner, which is every package that analyze installs, and the attestations of the provenance of those that have them, while the builds of the command line for other systems are pinned by their hash alone, and installs once more from a copy of the lock file with another hash of the command line, which npm must refuse. It runs every week as well.
  • Why analyze doesn't verify them itself. The hashes of the lock file bind every package to the one that the check verified, so a verification in every run would find nothing new. It would cost a host: npm reads Sigstore's root of trust from tuf-repo-cdn.sigstore.dev for the attestations, which would join the firewall policy of the job with the secret in every repository. So the policy keeps registry.npmjs.org alone for npm, and only the check engine contracts lets the root of trust through.
  • Codex and Claude come with their actions, which install their command lines themselves: Codex at the version of codex-version, Claude Code at the one of the action's release, both pinned for Renovate.