Detection Model
Grackle does not match on a blocklist of repositories or agent names alone. It reasons structurally about three properties of a workflow, per job, and only reports when all three hold at once.
1. Fork-reachable trigger
The workflow runs on an event an outside contributor can cause:
pull_request_target, pull_request, issue_comment, issues,
pull_request_review, pull_request_review_comment, a reusable
workflow_call reached from one of these, or a workflow_run consumer chained
off a fork-PR-triggered producer that ingests the fork run’s data (artifact,
head_sha, or linked PR) without a same-repo guard.
A job is not reachable when its own if: confines it to a non-fork event
(github.event_name == 'push' | 'schedule' | 'workflow_dispatch',
pull_request.merged == true, a protected-branch ref) or hard-disables it with
if: false. A guard on a sibling job never suppresses the agent job.
2. No author gate
Nothing restricts who may trigger the agent. Grackle recognizes the common gates and treats their presence as safe:
author_associationinOWNER/MEMBER/COLLABORATORgetCollaboratorPermissionLevel/permission.permissionchecks- owner-equality (
github.actor == github.repository_owner) - allowlists (
contains(vars.ALLOWED_USERS, ...login)) - fork-exclusion checks and maintainer-only label gates
3. Write-capable agent job
The job that runs the agent can mutate the repository: it declares
contents: write / permissions: write-all, inherits a workflow-level write
default it does not narrow, pushes directly (git push, gh pr create|merge),
grants the agent exec/write tools (Bash, Edit, Write), or runs the agent
in an auto-approve mode (--dangerously-skip-permissions, --yolo,
--permission-mode bypassPermissions).
How rules are structured
Each rule is pure data: an anchor regex that locates an agent invocation, an evaluation family that decides the post-filter, shared compliance metadata, and self-contained positive and negative examples. A single engine owns evaluation, so rules never reach into scanner internals.
- Installed rules anchor on an agent CLI or action name and confirm the family with a whole-file proof, so a bare binary name does not match unrelated tooling.
- Action rules anchor on an action configured to open itself to forks (a
write sandbox,
allowed_non_write_users: "*", YOLO/auto approval). - GitLab rules use GitLab-native reachability, gating, and write-capability
detection rather than the GitHub
on:/permissions:model.
Composite / local actions
An agent is sometimes hidden one level down: the workflow step is
uses: ./.github/actions/foo and the real invocation lives in that action’s
action.yml. When grackle scans a directory it resolves each local
uses: ./<path> reference off disk and inspects the referenced action, attributing
any agent it finds to the caller workflow at the uses: line. Reachability,
gating, and write capability still come from the caller job, since that is where
the trust boundary and token live. Resolution is one level deep.
GitLab CI
GitLab’s fork-pipeline model differs from GitHub Actions: a fork merge-request
pipeline runs in the fork with the fork’s variables, so the parent’s protected
secrets are not injected by default. A .gitlab-ci.yml agent job is flagged
(HIGH) when it runs on merge-request content, is handed write or execute
capability, and is not self-gated. It is HIGH rather than CRITICAL because final
exploitability also depends on project settings not visible in the file. GitLab
findings are suppressed by a fork-ID guard, a source-branch-name gate, a job
gated on a project-internal variable, read-only tools, --permission-mode plan
with no offsetting write capability, or a fork-scoped CI_JOB_TOKEN.