← Back to blog
Use CasesCI10 min read

August 5, 2026

Evaluate before CI loads credentials

A fork pull request shared a job with configure-cloud-credentials. Nobody had stolen anything — the PR was a typo fix. GitHub already withholds secrets from pull_request on forks. That was not the hole. The workflow mixed untrusted checkout with a credential step (pull_request_target / same job as tests). If the next PR had been worse, production credentials could have been minted where untrusted code ran. The gap was evaluate-shaped, not a breach.

Separate untrusted work from credentials

Teams mix untrusted work and credential work in one workflow, enable pull_request_target because tests "need secrets," or load production credentials before classifying the trigger. GitHub's primitives are repo- and environment-level. They are not an evaluate of ci_privilege vs deploy.

Coding agents make the typo-PR shape ordinary. Without evaluate before secrets, a confused agent can hammer a deploy path that should have been deny or approval_required.

Evaluate before secrets, production credentials, or prod credentials

Carry owner/repo@sha, the GitHub event name, and the action type (ci_privilege for tests/lint, deploy for production publish). Policy returns allow, deny, or approval_required. On approval_required, fail closed until an operator approves.

- uses: limetry/limetry/packages/ci@main
  with:
    limetry_api_key: ${{ secrets.LIMETRY_API_KEY }}
    limetry_base_url: ${{ vars.LIMETRY_BASE_URL }}
    policy_id: ${{ vars.LIMETRY_CI_POLICY_ID }}
    action_type: deploy
    require_trusted: "true"

If the event is pull_request (untrusted) and require_trusted is set, the step fails immediately — before configure-cloud-credentials or docker login. The failing step produces a structured error: "action_type deploy is denied; event pull_request is not trusted."

Split ci-privilege and deploy policies

  1. ci-privilege.json — allows ci_privilege (tests, typecheck, lint). Denies deploy. Used in the quality-gate workflow every PR triggers.
  2. deploy.json — allows ci_privilege and deploy. Only reached by workflows that require a trusted event (push to main, release, or workflow_dispatch).

PRs evaluate one intent and pass. Production deploy requires both a trusted trigger and an explicit policy allow.

Classify GitHub events

@limetry/ci classifies events against an explicit trusted allowlist:

  • Trusted: push, release, workflow_dispatch
  • Untrusted (named): pull_request, pull_request_target, pull_request_review, pull_request_review_comment, issues, issue_comment, workflow_run
  • Everything else, including schedule and future events: untrusted by default. A ref of refs/pull/* is also untrusted regardless of event name.

require_trusted: true on a deploy step fails the job before configure-cloud-credentials.

Use the composite action or the library

@limetry/ci (open source) ships as a GitHub composite action and a TypeScript library. The composite action can boot a local Limetry node in the job, apply policy JSON, and evaluate. The library exposes evaluateCiPrivilege() for custom scripts or a remote self-run evaluate endpoint.

import { evaluateCiPrivilege } from "@limetry/ci"

const result = await evaluateCiPrivilege({
  apiKey: process.env.LIMETRY_API_KEY!,
  policyId: process.env.LIMETRY_POLICY_ID!,
  agentId: "github_actions",
  actionType: "deploy",
  repository: "acme/api",
  sha: process.env.GITHUB_SHA!,
  eventName: "push",
})

if (result.evaluation.decision === "deny" || result.trust === "untrusted") {
  process.exit(1)
}

Lessons from dogfooding

We run @limetry/ci on the Limetry monorepo. Quality-gate evaluates ci_privilege (must allow) and deploy (must deny). Deploy evaluates deploy with require_trusted: true before cloud deploy credentials load.

Keep the gate fast. Starting a Limetry node inside the job adds seconds. Build the server before the gate step, or point limetry_base_url at a standing self-run evaluate endpoint to avoid local node startup.

Write clear failure messages. Contributors should see: "Limetry denied action_type=deploy because event=pull_request is untrusted."

Stage before required checks. Run as a non-required job, read decisions from audit, then make the check required at the branch-protection layer. The action still fails the step on deny.

Branch protection and environment rules remain. Limetry is the evaluate layer on top: allow ci_privilege on any event, but deploy needs a trusted event and policy allow, with privacy-safe audit across repos.

Related: The Copilot PR burst that nearly shipped to prod and Staging the CI gate.

Run Limetry on your own stack

Self-host the open source evaluation server, wire evaluate into your agents, and keep privacy-safe audit under your control.