Private Taps
Petros Amoiridis
Enterprise
Private Taps are Homebrew Tap repositories hosted in private GitHub or GitLab repositories. Workbrew syncs these repositories through a GitHub App installation or GitLab OAuth integration and distributes them to managed Devices.
Repository structure
A repository must meet the following requirements for Workbrew to discover and sync it:
| Requirement | Detail |
|---|---|
| Name prefix | Must start with homebrew- (e.g. homebrew-tools, homebrew-internal) |
| Visibility | Must be private. A GitLab repository set to internal is not synced |
| Formula directory | Formula/, HomebrewFormula/, or the repository root, in that order of precedence. Only the first one that exists is read, so an empty Formula/ directory hides formulae kept in the root. Variants such as Formulas/ or formula/ are not recognized |
| Cask directory | Casks/ (capital C, plural). There is no fallback. Variants such as Cask/ and casks/ are not recognized, and casks in the repository root are not discovered |
| Formulae in the repository root | Only .rb files that define a Formula subclass are read as formulae. Helper scripts alongside them are skipped |
| Subdirectories | Only the top level of each directory is read. See Sharded directories below |
Homebrew recommends keeping formulae in Formula/ rather than the repository root, so that they are not mixed in with other top-level files. See How to Create and Maintain a Tap in the Homebrew documentation.
Sync behavior
Every five minutes, the Workbrew Console syncs with the GitHub or GitLab API to discover private repositories whose name starts with homebrew-. For each repository, it fetches the latest commit on the default branch and parses the formulae and casks from the directory tree.
The initial sync runs automatically when a Tap is first discovered. After that, package re-syncs only run when an admin clicks Sync Now in the Console. Automatic re-sync does not run for Taps that have already been synced at least once.
A newly discovered Tap is enabled automatically only when it is the first Tap in the Workspace. Every Tap discovered after that arrives disabled and reaches no Device until an admin enables it in the Console.
How Taps reach Devices
For GitLab Taps, the Console creates an AutoTapCommand that schedules brew tap <name> <git_url> on target Devices. The Agent runs this command to clone the repository, and authenticates the clone with the git-credential-workbrew credential helper. See How Workbrew authenticates private Taps for details on the credential flow.
For GitHub Taps, the Console sends the Device a tarball of the repository at its synced commit, and the Agent extracts it into the Tap directory.
A Tap that the Agent has cloned over git is never overwritten by a tarball. A Tap installed from a tarball is downloaded again and replaced whenever its commit changes in the Console.
GitLab nested paths
GitLab repositories can live in nested subgroups (e.g. acme/infra/tools/homebrew-utils). Workbrew derives a two-segment Homebrew Tap name from the full path by taking the root namespace and the repository name:
| Repository path | Derived Tap name |
|---|---|
acme/homebrew-tools | acme/homebrew-tools |
acme/infra/tools/homebrew-utils | acme/homebrew-utils |
acme/team-a/homebrew-tools | acme/homebrew-tools |
The full repository path is stored separately and used for GitLab API calls and clone URLs.
If two repositories in different subgroups derive the same Tap name (e.g. acme/team-a/homebrew-tools and acme/team-b/homebrew-tools), only the first is synced. See Tap name collisions below.
Known constraints
Sharded directories
Homebrew supports sharded Tap layouts, where formulae are grouped into single-letter subdirectories such as Formula/a/ack.rb. Workbrew reads only the top level of Formula/, HomebrewFormula/, and Casks/, so packages held in subdirectories are not listed in the Console. Devices can still install them, because Homebrew reads the cloned repository directly. The symptom is a Tap that shows no packages in the Console while brew install works on a Device.
Default branch changes
If the default branch of a Tap repository is changed after a Device has already cloned it, the Device retains the old branch. Running brew untap --force <tap-name> on the affected Device removes the stale clone. The --force flag is required when packages from the Tap are currently installed. The Tap is re-cloned on the next sync cycle, and Homebrew re-associates the installed packages with it.
Untapping with installed packages
brew untap refuses to remove a Tap if formulae or casks from that Tap are currently installed. brew untap --force <tap-name> overrides this and removes the Tap regardless. Installed packages remain on the Device. Their install receipts still reference the Tap, but the packages cannot be upgraded from it until the Tap is re-cloned.
Disabling vs. removing from Devices
Unchecking a Tap in the Console disables it, which stops future installations on new Devices. It does not remove the Tap from Devices that have already cloned it. There is no remote untap mechanism from the Console.
Tap name collisions (GitLab)
Two GitLab repositories with the same name under different subgroups (e.g. acme/team-a/homebrew-tools and acme/team-b/homebrew-tools) derive the same Tap name (acme/homebrew-tools). Only the first repository encountered is synced. Renaming one of the repositories resolves the collision.
Related docs
- Create a private Tap with a custom formula and cask — step-by-step tutorial for building a Tap from scratch
- Sync and authenticate private Taps with Workbrew — setup instructions for GitHub and GitLab integrations
- How Workbrew authenticates private Taps — how the credential helper and token flow work
- Authenticate private artifacts downloaded by a formula or cask — send a token from a formula or cask that downloads a private binary
- Network requirements — domains that must be reachable for Tap clones and updates