Skip to content

Public vs Private GitHub Repositories: What Visibility Really Means

Lesson 3 of 10Beginner10 min readGitHub Engineering · GitHub FundamentalsVerified: GitHub.com and gh 2.98.0, August 2026

Visibility is the single most consequential setting on a GitHub repository, and it is chosen in a form field that most people click past in under a second.

The choice looks binary and simple. It is neither. It affects who can read your code, who can fork it, which features are available, how Actions minutes are billed, whether Pages can publish, and — most importantly — what happens to everything you have ever committed if the setting changes.

Public. Anyone on the internet can read the repository, clone it, and fork it. No account is required to browse it. Search engines index it.

Private. Only the owner and people explicitly granted access can see it. It does not appear in search, and its URL returns a 404 — not a 403 — to anyone without access, so its existence is not disclosed.

Internal. Visible to all members of the owning enterprise, and private to everyone else. It is a middle setting for organisations that want code shared across the company without publishing it.

Private does not mean secrets can safely be committed.

This is worth stating flatly because the intuition runs the other way. Private feels like a safe box. It is not a safe box; it is an access-control list, and access-control lists change.

Consider what a committed API key actually is once it exists in history:

  • It is in every clone, on every machine anyone has ever cloned to, including laptops that have since been lost, sold or compromised.
  • It is in every fork made while it was reachable, and forks in a network can survive the deletion of the original.
  • It is in CI logs and caches, potentially, depending on what your workflows print.
  • It is one setting change away from public, and one compromised account away from disclosure.
  • It is in backups and mirrors you may not control.

And the fix that feels natural — commit a change removing it — does nothing. The old commit still contains it. Git history is append-only by design; removing a file in a later commit is a new snapshot, not an erasure.

The correct model is that repository visibility is access control, not secret management. Secrets belong in a secret store, in environment variables supplied at runtime, or in GitHub’s encrypted secrets — never in the tree, at any visibility.

PublicPrivate
Who can readAnyoneExplicit collaborators only
Discoverable in searchYesNo
Forkable by strangersYesNo
Cost of Actions minutesFree for standard runnersConsumes the account’s included minutes and billing
GitHub PagesPublishableDepends on plan
Some collaboration featuresGenerally availableAvailability varies by plan
Consequence of a mistakeImmediate and publicContained, but not permanent

The bottom row is the honest summary. Public repositories punish mistakes instantly. Private repositories defer the punishment, which is better but is not the same as preventing it.

It is worth understanding why private feels safer than it is, because the same reasoning error shows up elsewhere.

A private repository has a strong perimeter and no internal compartments. Everyone with read access gets everything: every file, every branch, every commit ever made. There is no way to grant someone access to the current tree but not to history, or to one directory but not another. Access is all-or-nothing at the repository boundary.

That is fine for code. It is the wrong shape for secrets, which need to be readable by a process at runtime and by almost no humans at all. A credential in a private repository is readable by every contractor, every new hire on their first day, every CI job, and every integration you have ever authorised — because all of them have repository read access, and repository read access is the only granularity there is.

Compare that with how a secret is supposed to work:

Committed to a private repoIn a secret store
Who can read itEveryone with repository accessOnly what you grant, per secret
RotationRequires a commit, and history keeps the old valueReplace the value; nothing retains it
Audit trailGit history shows changes, not readsReads are logged
ExpiryNoneUsually supported
Blast radius of a leaked cloneTotalNil — the clone contains no secret
RemovalImpossible; only rotation helpsDelete it

Every row favours the right-hand column, and the last one is decisive. Anything committed is permanent. Secret management is precisely the discipline of keeping values out of permanent, widely-replicated storage.

Three mechanisms cover almost every case, and none of them involve the repository tree.

Environment variables supplied at runtime. The process reads os.environ or equivalent; the value comes from the platform running it. Nothing is stored in the repository, and a leaked clone contains nothing useful.

GitHub encrypted secrets for anything running in Actions. Values are encrypted at rest, exposed to workflows as environment variables, and masked in logs. They are set per repository, per environment or per organisation, and they never appear in the tree.

A dedicated secret manager — your cloud provider’s, or a self-hosted one — for production systems. These add per-secret access control, audit logging, automatic rotation and versioning, none of which Git can offer.

The pattern in all three is the same: the repository holds the name of the thing, and the environment supplies the value.

Terminal window
# In the repository: a template that documents what is needed.
# .env.example — committed, contains no values
DATABASE_URL=
GITHUB_TOKEN=
STRIPE_SECRET_KEY=

Then .env itself goes in .gitignore, and the real values are supplied per machine or per deployment. Committing .env.example and ignoring .env is a small convention that prevents a large category of accident.

Visibility interacts with more of GitHub than most people expect, which is another reason to choose it deliberately rather than change it later.

GitHub Actions. For public repositories, standard hosted runners are free. For private repositories, minutes are billed against the account’s allowance, which means a chatty workflow on a private repository has a cost that the same workflow on a public one does not. Workflows triggered by pull requests from forks are also deliberately restricted, because otherwise anyone could run arbitrary code with your repository’s credentials.

GitHub Pages. Publishing from a private repository depends on plan; publishing from a public one generally does not. Note also that a Pages site is public even when built from a private repository — the repository stays closed, the output does not. That catches people out.

Package registries. A package’s visibility is linked to its repository. Making a repository private does not automatically make an already-published public package private.

Search and code navigation. Public repositories are indexed by GitHub’s code search and by external search engines. Anything in a public repository should be assumed to be findable by string, not merely readable by someone who already knows the URL.

Forks follow rules that surprise people.

A public repository can be forked by anyone, and the fork is public. You cannot prevent this while remaining public; it is what public means.

A private repository can only be forked by users who already have access, and the fork inherits the private visibility. Organisations can disable private forking entirely.

The rule that causes real trouble concerns fork networks. Repositories in the same fork network share an object store. That has an important consequence: a commit pushed to a fork of a public repository can remain accessible through the upstream repository’s network, even if the fork is later deleted, and even if the commit was never merged. Objects reachable in a public network tend to stay reachable.

This means “I pushed the secret to my fork, not the real repository, and then deleted the fork” is not the containment it sounds like. Rotate the credential.

Both directions are possible and both carry consequences.

Everything becomes readable — including the entire history, every branch, every tag, every commit message, and every file that ever existed in the tree.

Before flipping this switch:

  1. Audit history for credentials, not just the current tree. Automated secret scanning helps, but assume it will not catch everything — internal hostnames, customer names and personal data are not patterns a scanner recognises.
  2. Read the commit messages. People write things in commit messages they would not put in a README.
  3. Check for internal references: ticket URLs, staging domains, employee names, architecture details you would rather not publish.
  4. Rotate anything that was ever a live credential, whether or not you think it is still valid.
  5. Decide on a licence. Public with no licence means nobody may legally use it.

Less dramatic but not free. Existing forks remain public — making the original private does not retract copies that already exist. Stars and watchers may be affected, and anything that depended on anonymous access, such as a package fetched directly from the repository or an unauthenticated CI job, will break.

Terminal window
gh repo edit username/my-project --visibility private

Default to private for anything work-related, anything with a customer in it, and anything you are not certain about. Going public later is a deliberate act with an audit; going private later does not un-publish.

Choose public when the code is intended for others to use or learn from, when you want contributions, when it is a portfolio piece, or when the project is genuinely open source with a licence to match.

Choose internal when the audience is your whole company and no wider — shared libraries, internal tooling, platform components.

A useful test: what is the worst outcome if a stranger reads every line of this, including its history? If the answer is “nothing, it is just code”, public is fine. If you find yourself qualifying the answer, it is not.

Treating private as a secret store. The central error this lesson exists to correct.

Making a repository public without auditing history. The current tree is clean; commit 47 is not.

Assuming a deleted fork erases its commits. Fork network objects can outlive the fork.

Removing a secret in a new commit and considering it handled. The old commit is still there, and still distributed. Rotate.

Forgetting the licence when going public. Published without a licence is not open source.

Not knowing which repositories you own are public. Audit periodically:

Terminal window
gh repo list --visibility public --limit 100 --json nameWithOwner,updatedAt

Use a disposable repository.

  1. Create a private repository and push two commits.
  2. Add a file containing an obviously fake credential such as API_KEY=not-a-real-key-12345, commit it, then remove the file in a second commit.
  3. Confirm the “removed” value is still in history: git log -p -- <file> shows it plainly.
  4. Reflect on the fact that at this point, in a real repository, the credential would need rotating regardless of visibility.
  5. Change the repository to public with gh repo edit --visibility public, view it while signed out, and confirm the history is fully readable.
  6. Delete the repository.

Step 3 is the whole lesson in one command. Seeing the value still there after “removing” it is more convincing than any warning.

  • Visibility is access control; it says nothing about whether committed secrets are safe.
  • Committed credentials are distributed to clones and forks and must be rotated, not deleted.
  • Private-to-public exposes complete history, not just the current state.
  • Public-to-private does not retract existing forks.
  • Fork networks share objects, so pushing to a fork of a public repository is effectively publishing.
  • Feature availability and Actions billing differ by visibility and plan.
Free Git Engineer Cheat Sheet BundleSix printable references plus downloadable toolkit files — commands, recovery, aliases, a CODEOWNERS starter. No email required.