Untrusted pull requests
manni docevals runs code that content files ask it to run. On a pull request from a fork, the person supplying those content files is a stranger.
Read this before enabling the gate on a public repository.
One path from a content file to code on your runner
Section titled “One path from a content file to code on your runner”A page can declare a command eval in its frontmatter:
evals: - id: check grader: command command: [node, scripts/whatever.mjs, "{file}"]That command runs only under execution.allow: [frontmatter-commands] (config) or
--allow-execution frontmatter-commands (CLI). It is denied unless you grant it, and a denied command
eval reports as skipped.
A command eval defined in manni.config.yaml needs no grant, because the config is yours, not
the content’s. On a pull request, though, the config is part of the change too.
The complete answer is to gate the job
Section titled “The complete answer is to gate the job”Flags are not sufficient. Restrict the job that can execute content-driven code to same-repo pull requests:
jobs: verify-docs: runs-on: ubuntu-latest # SECURITY GATE. Do not remove. if: >- github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository steps: - uses: actions/checkout@v7 # ... full run, with secrets availableThen give forks a separate job that runs no page-declared command and needs no secret:
verify-docs-fork: runs-on: ubuntu-latest if: >- github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository steps: - uses: actions/checkout@v7 - uses: actions/setup-node@v6 with: { node-version: 24, cache: npm } - run: npm ci - run: npx @hawkeyexl/manni docevals run --deterministic-only --no-execution --format githubFork contributors still get every tool:regex check with no credential in reach. A command eval
in the fork’s manni.config.yaml still runs there, as anything in the fork’s workflow would. That
job holds no secret and gets a read-only token, so it has nothing worth stealing.
Frontmatter and structure are checked by their own domains. Add manni meta validate and
manni lint structure as steps in the same job. Neither runs content-authored code.
Why pull_request_target is the wrong fix
Section titled “Why pull_request_target is the wrong fix”It runs with repository secrets and can be made to check out the fork’s code. That combination is how tokens leak. If you are reaching for it to give forks a full run, stop: the deterministic job above is the safe version of what you want.
Secrets are unavailable on forks by design
Section titled “Secrets are unavailable on forks by design”GitHub does not expose secrets to fork pull requests. That is a feature. It also means the fork path
must work without a provider, which --deterministic-only does.
If your gate is unusable without a key, fork contributors get no signal at all, and you will be tempted toward the unsafe workaround above.
A checklist before going public
Section titled “A checklist before going public”- The job that can execute content code is gated to same-repo pull requests.
- Forks run a separate
--deterministic-onlyjob with no secrets. execution.allowgrants only what the corpus needs, and nothing on a fork-facing job. That is defence in depth, not the defence.- Third-party actions pinned to a full commit SHA, not a tag.
permissions:set to least privilege (contents: readfor a check job).- A turn budget set (
judge.maxTurns), so a malicious or accidental change cannot run up a bill.
Reviewing changes to the gate itself
Section titled “Reviewing changes to the gate itself”A pull request that edits the workflow is asking for a change to what runs with your credentials.
Review those with the same care as a change to authentication code. Remember too that a contributor
who can add a command eval to manni.config.yaml, or a grant to execution.allow, has changed
what the whole job runs. That holds even if the workflow file is untouched.