# Repository

> A repository is a versioned store of a project's files and full history; in Git, a content-addressed object database plus mutable refs.

Also known as: code repository, source code repository, Git repository, codebase

Source: https://socket.dev/glossary/repository

Last updated: 2026-11-04

A repository (repo, code repository) is a versioned store of a project's files and their full history. A Git repository is a content-addressed database of blobs, trees, and commits, plus refs naming the latest commits. Repositories are where code is written and reviewed, but most package managers install a separate artifact, and that gap is a supply chain risk.

A codebase is the set of source files that make up a program at one moment. A repository is that codebase plus every earlier version and the metadata linking them. The same word also names a [package repository](https://socket.dev/glossary/artifact-repository) such as npm, PyPI, or Maven Central, which stores built, versioned artifacts rather than source history. Supply chain security lives in the space between the two.

Our position: a source repository is evidence about a package, not the package. Review and scanning must target the artifact your package manager actually installs. We measured the gap on a popular, well-run project: 505 of the 840 files in zod 4.6.7, including both of its entry points, exist nowhere in its repository at the commit its provenance names.

## What is a code repository?

A code repository is a directory tree plus a database of its history, managed by a [version control](https://socket.dev/glossary/version-control) system so every change is recorded and reversible. Centralized systems such as Subversion keep that history on one server. [Git](https://socket.dev/glossary/git), which dominates today, is distributed: every clone is a complete repository, and "remote" is only a name for another copy you fetch from and push to.

Hosting platforms such as [GitHub](https://socket.dev/glossary/github) wrap a repository with accounts, access control, pull requests, CI runners, and an audit log. None of that is part of the repository. Branch protection rules, review requirements, and the record of who pushed what live in the host's database, and a clone carries none of it. That distinction decides what a repository can and cannot prove, which is the subject of the rest of this entry.

## What is a Git repository?

A Git repository is a content-addressed object store plus a small set of mutable pointers called refs. [Git's own glossary](https://git-scm.com/docs/gitglossary) defines it as a collection of refs together with an object database containing every object reachable from them. The fastest way to see the structure is to build one:

```bash
$ export GIT_AUTHOR_DATE=2026-11-04T08:00:00-08:00 GIT_COMMITTER_DATE=2026-11-04T08:00:00-08:00
$ git -c init.defaultBranch=main init -q
$ git config user.name "Ada Dev" && git config user.email ada@example.com
$ printf 'console.log("hi")\n' > index.js
$ git add index.js && git commit -qm "first commit"

$ find .git/objects -type f
.git/objects/1a/c74b42183215c938310f9bd404c60ee49770d5
.git/objects/89/ce8421df5b75ea6b53cb147879202275b1d985
.git/objects/ba/5beffecdf48d977e636b4889d77fee4c4959c6

$ cat .git/HEAD
ref: refs/heads/main
$ cat .git/refs/heads/main
89ce8421df5b75ea6b53cb147879202275b1d985

$ git cat-file -p HEAD
tree ba5beffecdf48d977e636b4889d77fee4c4959c6
author Ada Dev <ada@example.com> 1793808000 -0800
committer Ada Dev <ada@example.com> 1793808000 -0800

first commit

$ git cat-file -p 'HEAD^{tree}'
100644 blob 1ac74b42183215c938310f9bd404c60ee49770d5	index.js

$ printf 'blob 18\0console.log("hi")\n' | sha1sum
1ac74b42183215c938310f9bd404c60ee49770d5  -
```

One commit produced three objects. A blob holds file bytes. A tree maps names and modes to blob and subtree hashes. A commit names one tree, its parent commits, an author, a committer, and a message. Each object's name is the SHA-1 hash of its type, length, and contents, which is why `printf | sha1sum` reproduces the blob name exactly.

Content addressing has a precise consequence. An object cannot change without getting a new name, and because each commit names its tree and parents, a commit hash covers the entire history behind it. Git history is therefore tamper-evident, not tamper-proof.

Refs are the opposite. A loose branch ref is a 41-byte file holding a hash, and anyone who can push can point it somewhere else. `HEAD` is a symbolic ref naming the current branch. Tags are refs too, and a force push can move a tag just as it moves a branch. The attacks in the rest of this entry all manipulate refs or metadata; none needs to break a hash.

As of November 2026, new Git repositories still default to SHA-1 object names. Git's [BreakingChanges](https://git-scm.com/docs/BreakingChanges) document commits Git 3.0 to SHA-256 for new repositories, and states there is no planned release date.

> Figure 1. The repository from the demo above. Changing any object changes its name; changing a ref changes nothing but the pointer.
>
> Git repository from the demo: HEAD and refs/heads/main are mutable pointers; two commits, one tree, and one blob are immutable objects named by their SHA-1 hashes.

Figure 1 shows the demo repository after one more commit. On the left are the two refs: `HEAD`, which contains `ref: refs/heads/main`, and `refs/heads/main`, which contains the hash of commit `edc0d41` and can be rewritten by any push. On the right are four immutable objects. Commit `edc0d41` claims author "Trusted Maintainer", a date of 2024-01-15, and the message "chore: bump deps". Its parent is commit `89ce842`, by "Ada Dev", dated 2026-11-04, message "first commit". Both commits name the same tree, `ba5beff`, because the second commit changed no files, and Git stored that tree once. The tree has one entry, `100644 blob 1ac74b4 index.js`, and the blob holds `console.log("hi")`. The blob's name is the SHA-1 of the header `blob 18`, a null byte, and those bytes.

## Commit identity is self-asserted

Every field in a Git commit object, including the author's name, email, and timestamp, is text the committer chose. Here is how the second commit in Figure 1 was made:

```bash
$ GIT_AUTHOR_DATE=2024-01-15T09:00:00Z GIT_COMMITTER_DATE=2024-01-15T09:00:00Z \
  git -c user.name="Trusted Maintainer" -c user.email=maintainer@example.org \
  commit -q --allow-empty -m "chore: bump deps"

$ git log --format='%h %an <%ae> %ad %s' --date=iso
edc0d41 Trusted Maintainer <maintainer@example.org> 2024-01-15 09:00:00 +0000 chore: bump deps
89ce842 Ada Dev <ada@example.com> 2026-11-04 08:00:00 -0800 first commit

$ git log -1 --format='%G?'
N
```

The newer commit claims to be two years older than its own parent, and Git accepted it. `%G?` prints `N`: no signature, and Git does not check one unless asked. Attackers use exactly this. The [PolinRider](https://socket.dev/supply-chain-attacks/polinrider) campaign force-pushed rewritten history with anti-dated commits, so compromised repositories showed months-old, routine-looking changes on their GitHub landing pages. The evidence was in GitHub's Activity view, which recorded the recent force push.

The [LiteLLM compromise](https://socket.dev/blog/litellm-attack-timeline) shows the other half. An attacker with push access created throwaway branches named `ci-tmp-<hex>` on October 10 and 12, 2026, pushed one commit each adding `.github/workflows/cache.yml`, let the push trigger run the workflow with the repository's secrets, and deleted the branches within minutes. Deleting a ref does not delete objects, so the commits survived, but nothing in the visible history pointed at them. The reconstruction came from Actions metrics and environment deployment records, which live in GitHub, not in Git.

Signed commits narrow this gap without closing it. A valid signature proves that whoever held a particular key signed that commit. It does not prove the key was not stolen, that the code is benign, or that the commit is what a registry later serves. The authoritative record of who did what is the host's audit log, and only the host has it.

## Is a published package the same as its repository?

A published package is not its repository. It is a separate artifact, built and uploaded by whoever holds publish rights, and most registries never check it against any repository. On npm, the `files` field selects what gets packed, build steps generate output that is usually gitignored, and the result is uploaded as a tarball. The `repository` field in `package.json` is a URL the publisher types. npm's [package.json documentation](https://docs.npmjs.com/cli/v11/configuring-npm/package-json) describes its format and how npm normalizes it at publish time, and describes no verification.

Unverified metadata is the mechanism behind [starjacking](https://socket.dev/glossary/starjacking). Three typosquats of `node-telegram-bot-api` in the [Telegram bot typosquat campaign](https://socket.dev/blog/npm-malware-targets-telegram-bot-developers) pointed their homepage at the real project's GitHub repository, borrowing its 19,000+ stars for the npm sidebar while their own code planted SSH keys.

To measure the gap on honest packages, we compared two npm tarballs, file by file, against the exact commit their provenance attestations name. The script runs anywhere with Git and curl:

```bash
#!/usr/bin/env bash
# usage: compare.sh <pkg> <version> <repo-url> <commit> <dir-in-repo>
set -euo pipefail
PKG=$1 VER=$2 REPO=$3 COMMIT=$4 DIR=${5:-.}
git init -q src
git -C src fetch -q --depth 1 "$REPO" "$COMMIT"
git -C src -c advice.detachedHead=false checkout -q FETCH_HEAD
mkdir pkg
curl -s "https://registry.npmjs.org/$PKG/-/${PKG##*/}-$VER.tgz" | tar xz -C pkg
same=0 changed=0 notinrepo=0
while read -r f; do
  if [ ! -f "src/$DIR/$f" ]; then notinrepo=$((notinrepo+1))
  elif cmp -s "src/$DIR/$f" "pkg/package/$f"; then same=$((same+1))
  else changed=$((changed+1)); fi
done < <(cd pkg/package && find . -type f | sed 's|^\./||')
echo "identical=$same modified=$changed not_in_repo=$notinrepo"
```

```bash
$ ./compare.sh semver 7.8.6 https://github.com/npm/node-semver \
    b37b033b585ad5e575afeccadb426ea41bc6cea4
identical=54 modified=0 not_in_repo=0

$ ./compare.sh zod 4.6.7 https://github.com/colinhacks/zod \
    b500a2ee95134f81d74da7911d7378dd2853d042 packages/zod
identical=335 modified=0 not_in_repo=505
```

> Figure 2. What the registry serves versus what the repository holds, measured November 4, 2026.
>
> Files in two npm tarballs compared with the commit named in their provenance. semver 7.8.6: 54 of 54 files byte-identical to the commit. zod 4.6.7: 335 of 840 files byte-identical, 505 generated by the build and absent from the repository.

Figure 2 and the table below show the same results, measured on November 4, 2026, when both versions were the latest releases.

| Package | Attested commit | Files in tarball | Byte-identical to commit | Only in tarball | Repo files not shipped |
| --- | --- | --- | --- | --- | --- |
| semver 7.8.6 | b37b033 | 54 | 54 | 0 | 104 |
| zod 4.6.7 (packages/zod) | b500a2e | 840 | 335 | 505 (60%) | 6 |

semver is the clean case. Every one of its 54 shipped files is byte-identical to the attested commit, and the 104 files it leaves out are tests, benchmarks, CI configuration, and project docs. zod is the normal case for TypeScript. Its 505 generated files are 124 each of `.js`, `.cjs`, `.d.ts`, and `.d.cts`, plus 9 nested `package.json` files. Node resolves `import "zod"` to `./index.js` and `require("zod")` to `./index.cjs`, and neither file exists in the repository. Reading zod's source tells you about the code that produced 60% of the package, not about those bytes.

None of this is a criticism of zod. It shows that the build step is a trust boundary, and reviewing the repository stops at that boundary. The xz Utils backdoor, disclosed on March 29, 2024, crossed it. Andres Freund's [original report](https://openwall.com/lists/oss-security/2024/03/29/4) stated: "One portion of the backdoor is solely in the distributed tarballs." A modified `build-to-host.m4` line shipped in the release tarballs for 5.6.0 and 5.6.1, while the Git repository and GitHub's auto-generated source archives did not contain it.

The [Shai-Hulud worm](https://socket.dev/blog/ongoing-supply-chain-attack-targets-crowdstrike-npm-packages) skipped the repository entirely. With a stolen npm token, it downloaded a victim package's tarball, replaced `bundle.js`, added a `postinstall` script, repacked, and republished. Socket's list of affected package versions stood at 526 two days after the first compromise, and the malicious `bundle.js` never appeared in any of those projects' Git history.

## What provenance proves about a repository

[Package provenance](https://socket.dev/glossary/package-provenance) turns the repository claim into a signed statement: this tarball, by digest, was built by this CI workflow from this commit. npm serves the attestation from a public endpoint:

```bash
$ curl -s https://registry.npmjs.org/-/npm/v1/attestations/semver@7.8.6 \
  | jq -r '.attestations[] | select(.predicateType=="https://slsa.dev/provenance/v1")
           | .bundle.dsseEnvelope.payload' | base64 -d \
  | jq '{subject, workflow: .predicate.buildDefinition.externalParameters.workflow,
         source: .predicate.buildDefinition.resolvedDependencies[0],
         run: .predicate.runDetails.metadata.invocationId}'
{
  "subject": [
    {
      "name": "pkg:npm/semver@7.8.6",
      "digest": {
        "sha512": "f65a52494e0c972de6aedaeec0bb3578b2bafa0cdbcea78117044d8c88c4fbd2ee0e959d61cb6b4254377a3d489608bb8249e9b134673e82cff03a421ea1c827"
      }
    }
  ],
  "workflow": {
    "ref": "refs/heads/main",
    "repository": "https://github.com/npm/node-semver",
    "path": ".github/workflows/release.yml"
  },
  "source": {
    "uri": "git+https://github.com/npm/node-semver@refs/heads/main",
    "digest": {
      "gitCommit": "b37b033b585ad5e575afeccadb426ea41bc6cea4"
    }
  },
  "run": "https://github.com/npm/node-semver/actions/runs/41486847600/attempts/1"
}

$ curl -s https://registry.npmjs.org/semver/-/semver-7.8.6.tgz | sha512sum
f65a52494e0c972de6aedaeec0bb3578b2bafa0cdbcea78117044d8c88c4fbd2ee0e959d61cb6b4254377a3d489608bb8249e9b134673e82cff03a421ea1c827  -
```

The digest matches the tarball the registry serves, and that `gitCommit` is the commit we diffed above. Publishing with provenance also requires the `repository` field to match the publishing repository, so an attested package cannot claim someone else's repository there. It does nothing for the homepage field the Telegram typosquats used. What it does not do is vouch for the code. npm's [provenance documentation](https://docs.npmjs.com/generating-provenance-statements) says plainly that provenance "does not guarantee the package has no malicious code." A compromised maintainer account or poisoned workflow produces a valid attestation for a malicious build. Our [npm v12](https://socket.dev/blog/npm-12) analysis notes that the worm waves of the past year published through legitimate pipelines, some with valid provenance.

The stronger check is a reproducible build: rebuild the attested commit and compare digests. Adoption is thin. When vlt tested 5,000 high-impact npm packages with its [reproduce tool](https://socket.dev/blog/vlt-launches-reproduce) in February 2025, 5.78% rebuilt faithfully and 3.72% carried provenance attestations.

| Evidence | What it proves | What it does not prove | Use it for |
| --- | --- | --- | --- |
| repository or homepage field | Nothing; the publisher typed it | That the package was built from that repository | Navigation only |
| Git tag | A ref pointed at a commit when you looked | That it pointed there at publish time, as the BoltDB typosquat showed | Finding the source, not verifying it |
| Signed commit | Someone holding a specific key signed this commit | That the key was not stolen, or that the artifact matches | Attribution inside a repository |
| Provenance attestation | This digest was built by this workflow from this commit | That the commit, the workflow, or the build is benign | Minimum binding of a package to its repository |
| Reproducible build | Rebuilding the commit yields the same digest | That the commit itself is benign | Ruling out build-time injection like xz |
| Analysis of the artifact | What the installed code actually does | Anything about future versions | Every install decision |

Verdict: require provenance as the minimum binding between a package and its repository, use reproducibility where a project supports it, and analyze the artifact itself in every case. Never treat a `repository` field, a homepage link, or a tag as evidence. This is why Socket's scanner analyzes the package contents the registry actually serves rather than the repository a package links to, which is how it caught the [BoltDB typosquat](https://socket.dev/blog/malicious-package-exploits-go-module-proxy-caching-for-persistence) described below after its Git tag had been moved to clean code.

## Ecosystems where the repository is the package

In some ecosystems there is no separate artifact, and the repository is the distribution channel. [Go modules](https://socket.dev/glossary/go-modules) are fetched from version control, by default through proxy.golang.org, which caches each version permanently, and `go.sum` checksums pin what you resolved. The BoltDB typosquat turned that into persistence. The attacker published a backdoored `github.com/boltdb-go/bolt` v1.3.1, waited for the proxy to cache it in November 2021, then moved the Git tag to clean code. Anyone auditing GitHub saw nothing, while `go get` kept serving the backdoor for more than three years. Immutability is still the right design, because it stops a compromised upstream from silently replacing versions you already pinned. It means a cached malicious version stays malicious.

Composer packages on Packagist can also resolve straight from Git, including `dev-*` versions that track live branches. During the [nova-two-factor compromise](https://socket.dev/blog/polinrider-github-packagist), reported in September 2026, PolinRider's malicious code sat on the `dev-main` branch of a Packagist package with more than 700,000 downloads, so anyone tracking that branch received it with no registry credentials stolen. As of July 2026, Socket had tied 162 malicious release artifacts across 108 packages to PolinRider, including 80 Go modules and 10 Packagist packages.

npm supports Git dependencies too, and they have no immutable registry copy at all. npm v12, generally available since July 2026, stopped resolving them unless explicitly allowed with `--allow-git`. Socket raises a [Git dependency alert](https://socket.dev/alerts/gitDependency) when a dependency resolves to a remote Git URL, because a ref can be repointed after you review it.

## How do attackers abuse repositories?

Attackers abuse repositories in three ways: as code that runs when a developer opens or builds the project, as persistence in CI, and as storage for stolen data.

Running on open is PolinRider's signature. Its loaders hide in fake `.woff2` font files or in config files padded with whitespace past the edge of the screen, and a `.vscode/tasks.json` entry with `"runOn": "folderOpen"` executes them when the folder opens in a VS Code-compatible editor. A plain `git clone` does not run repository code, but opening, building, or testing an untrusted repository can.

CI persistence uses the workflow directory. Shai-Hulud wrote `.github/workflows/shai-hulud-workflow.yml` into repositories it could reach, so later CI runs could exfiltrate that pipeline's secrets. LiteLLM's attacker needed only a push to a throwaway branch, because a workflow triggered by `push` runs on any branch with full repository secrets. GitHub withholds secrets from workflows triggered by pull requests from forks, which is why both attacks required stolen push credentials rather than an outside pull request.

Storage is the simplest. Shai-Hulud created a repository named `Shai-Hulud` in victims' GitHub accounts to hold harvested credentials, and nearly 700 public repositories titled "Shai-Hulud Migration" appeared on GitHub at the same time. Ownership attacks complete the set: [repojacking](https://socket.dev/glossary/repojacking) re-registers a renamed or deleted account so old links and fetch URLs resolve to an attacker's repository.

Each control covers part of this:

- **Branch protection and required review** stop direct pushes to protected branches. They do not stop a push-triggered workflow on an unprotected branch, or an admin account that has been taken over.
- **Code owners on `.github/workflows/` and `.vscode/`** force review of the files attackers use for execution. They do nothing for payloads hidden in ordinary config files.
- **Secret scanning** catches known credential formats at push time. A secret that has already been pushed lives in the object store and in every clone, so the only fix is revocation. LiteLLM's own investigation traced initial access to a key committed to a public repository in May and never revoked.
- **Audit-log monitoring** is the only reliable record of force pushes, branch deletions, and token use. It exists only on the host, and it helps only if someone reads it.

The weak link is still the arrow from repository to artifact. Provenance draws that arrow with a signature, but only for a minority of packages and only as far as the build workflow. Until reproducible builds are routine, a repository is a claim about a package, and the only thing you can fully verify is the artifact you are about to run.
