GitHub Actions Workflow Execution Protections: The December 2 Default Skips Private Repos

GitHub moved automatic enforcement of its default pull_request_target rule to 2 December 2026, and it lands on public repositories only. Gheist runs in private repos on a push event, so the actor and event rules you write yourself are the ones that matter.

By VVV Ops ·

GitHub pushed the automatic enforcement date for its default pull_request_target rule from 2 November to 2 December 2026, first in an editor's note added to the GA changelog on 21 October and then in a changelog post of its own on 2 November. The extra month comes with two brownouts. On 9 November and again on 16 November, from 16:00 to 18:00 UTC, the rule enforces for two hours and then drops back to evaluate mode.

The rule is one small piece of a feature that reached general availability on 17 September: workflow execution protections, which is GitHub's allowlist layer for Actions. An actor rule says who may trigger a workflow. An event rule says which events may start one. Actions evaluates both before a run exists, so a blocked run never gets a runner, a token, or a log.

The December default is a sensible change and you should let it land. It also does almost nothing for the repositories where Actions secrets are being stolen this quarter. Those two statements get collapsed into a single argument, usually the wrong one.

The December 2 default covers public repositories only

The changelog is specific about scope. The default rule applies to public repositories that have no applicable Actions event policy of their own. Private and internal repositories are untouched. An existing policy takes precedence, so a team that already wrote one sees nothing change on 2 December.

The trigger GitHub picked is the obvious one. pull_request_target runs in the context of the base repository with access to its secrets, while the pull request it reacts to can come from any fork. Check out the wrong ref and you have executed fork code next to your production credentials.

Outside the brownout windows the default sits in evaluate mode. Runs still execute, and GitHub records which ones would have been blocked. Inside a window, a pull_request_target run the rule would block fails with an error annotation and does not execute. The rollout covers public repositories on GitHub Free, Team and Enterprise Cloud, and skips GitHub Enterprise Server.

Evaluate mode is where you find out what breaks

This is the part with a date on it, so do it before 9 November, when the first brownout lands. In a repository or organization, open Settings, then Policies under Actions in the left sidebar; policy insights sit directly under the policies page. The Enterprise Cloud docs walk through the same screens.

In our experience the legitimate uses of pull_request_target cluster in three places: bots that label or triage incoming pull requests, workflows that gate first-time contributors, and anything that has to write a comment back onto a fork's pull request. All three need a token the fork cannot have, which is why they were written this way. All three will fail during the brownouts and stop on 2 December unless a policy allows the event.

The decision per workflow is whether it needs the base repository's secrets at all. A labeler usually does not, and moves to pull_request with a read-only GITHUB_TOKEN. A workflow that posts a coverage comment does, and either gets an explicit allow or gets split into a pull_request job that uploads an artifact and a workflow_run job that comments.

What Gheist does with a stolen token

Wiz Research has been tracking a campaign it calls Gheist since 15 September 2026, and has recovered more than 13,500 planted branches across more than 850 compromised accounts. The mechanism is worth reading closely, because it explains why the December default is aimed at the other half of the problem.

Gheist starts from a stolen token, enumerates every repository that token can reach, and pushes a branch matching ^ci-sync-[0-9a-z]{6}$ carrying .github/workflows/sync-check.yml under the commit message ci: sync config. The workflow prints the environment and toJSON(secrets) through base32, which defeats log masking, since GitHub masks a secret only where it appears verbatim. The tool then downloads the job log through the GitHub API and deletes the run seconds later. Nothing leaves GitHub's own infrastructure, so there is no attacker domain to block at the egress layer.

The December default lands on the other half

Note the trigger. A pushed branch and a workflow file is a push event. The December default restricts pull_request_target, which this campaign never uses.

Public repositories are already covered, by a different mechanism: since 28 July 2026 GitHub holds potentially malicious workflow runs in public repositories until a collaborator approves them, and every public Gheist run Wiz examined was still sitting in that queue. The hold does not apply to private repositories or to GitHub Enterprise Server, and that is where Wiz observed the workflows running and then being deleted. In at least one enterprise environment, cloud credentials taken this way were later used to enumerate AWS Secrets Manager.

So the scopes are inverted. The control arriving on 2 December lands on repositories that another mechanism already covered, and skips the ones where the payload executes.

The actor rule is the one to write for private repositories

The same policy engine handles both halves, which is the useful part. The REST API takes POST /repos/{owner}/{repo}/actions/policies and POST /orgs/{org}/actions/policies, with name, enforcement set to disabled, active or evaluate, plus conditions and rules.

// Illustrative. Actor ids are placeholders; read them from the API for your own org.
{
  "name": "acme-private-actions-actors",
  "enforcement": "evaluate",
  "conditions": {
    "workflow_path": { "include": ["~ALL"], "exclude": [] }
  },
  "rules": [
    {
      "type": "restrict_actions_actors",
      "parameters": {
        "allowed_actors": [
          { "id": 1000001, "type": "Team" },
          { "id": 49, "type": "Bot" }
        ]
      }
    }
  ]
}

Set enforcement to evaluate first and read the insights, exactly as you would for the public-repo default. Evaluate mode is a GitHub Enterprise Cloud feature; on a Team plan, compare the actor list against the last 90 days of run history by hand before you set active. One trap: a workflow that runs under a built-in feature's identity needs that identity added as an allowed actor. dependabot[bot] is the one that catches people, and the docs say so in as many words. Actor types are User, Bot, Team, BusinessTeam, EnterpriseTeam, IntegrationInstallation, App and RepositoryRole.

This rule has a limit. A stolen token belonging to an engineer who is already in allowed_actors still triggers runs, and an actor rule will not see that. What it removes is the long tail: machine users, service accounts, former contractors, and bot installations whose credentials sit in infostealer dumps and which never needed to start a workflow. Across the engagements where we have looked, that list is longer than anyone expects, and it is the population Gheist's token sources are drawn from.

The event rule is the companion. restrict_action_events takes parameters.allowed_events from a list of 36 values, and a private-repo policy that omits push from that list blocks the Gheist workflow outright. Read your evaluate results before you do that, because the branches your release automation pushes are also push events.

Scoping a policy to workflow paths you recognize

conditions.workflow_path is where this gets misread. It is a condition on which workflows a policy applies to, not a list of files permitted to run. Both include and exclude are required arrays. ~ALL on its own in include matches every workflow and is not accepted in exclude. Omit workflow_path entirely on create and the policy targets all workflows without recording an explicit condition; omit it on update and the existing targeting is preserved.

The practical shape for a private repository is one broad policy with include: ["~ALL"] carrying the actor and event restrictions, and then narrow policies that exclude the handful of workflows with a documented reason to be looser. Written the other way round, with a short include list of the workflows you know about, a planted sync-check.yml simply falls outside the policy and runs unrestricted.

Organization-level conditions need one of repository_name, repository_id or repository_property alongside workflow_path. Repository-level conditions can be empty or hold workflow_path alone.

Where policies stop and credential hygiene starts

There is a third rule type, require lockfile, which blocks targeted workflows that are not onboarded to the repository's action lockfile. Read its limit before you count on it: it covers step-level uses references only. A reusable workflow called at job level is outside it, and if that reusable workflow is not onboarded in its own repository, the calling run is still blocked.

And a policy does not undo an exposure. Wiz's remediation list is the right one: rotate every repository, organization and environment secret that an affected repository could read, revoke the tokens that pushed the commits, check those tokens' owners for infostealers, and treat a self-hosted runner and its instance role as compromised if a ci-sync branch ever ran on it. Deleting the branch and the run changes nothing about what already left. The structural fixes sit below the policy layer too: GitHub Apps or short-lived fine-grained tokens in place of personal access tokens, ephemeral self-hosted runners, and OIDC trust policies tight enough that a repository name alone is not a passport into your cloud account. We wrote about drawing those lines in trust boundaries for AI agents in CI/CD, and about the registry-side version of the same problem in our npm dependency cooldown post. The staged rollout fits into the sequence in our DevSecOps pipeline guide.

If you want help with this, get in touch. The first thing we would pull is the list of accounts and bot installations that triggered a workflow run in your private repositories over the last 90 days, next to the list of accounts that can. The gap between those two columns is the actor rule you should write before you spend a day arguing about pull_request_target.

Tags: github actions workflow execution protections, devsecops implementation, security automation, integrate security into ci/cd pipeline, ci/cd pipeline automation, shift-left security