The gate
What the gate checks before anything can be published, and what to do when it says no.
A green gate means two things: your CI passed, and nothing that shouldn’t go public is in the code that would. Both are checked on the exact files a publish would send, never on your branch as it stands.
What the gate does
Section titled “What the gate does”ghostbranch gate <feature> runs these steps, and stops at the first one that fails:
- Rebase the feature onto public
main, in a throwaway copy. Your branch isn’t touched. A stacked feature is rebased with the branches below it. - Strip what never goes public: the overlay’s paths, such as
.ghostbranch/, and any internal sections (see below). What’s left is the candidate. - Scan the candidate for leaks.
- Push the candidate, plus the overlay branch’s files, to the gate branch
ghostbranch-gate/<feature>in the shadow repo, and open a draft pull request there. - Wait for every check on that pull request to finish, and record the result.
The scan runs before anything is pushed, so a leak never even reaches the shadow repo’s gate branch. And the gate never touches the public repo.
A feature with nothing to publish
Section titled “A feature with nothing to publish”Before it pushes anything, the gate checks that the feature has something to publish. A feature with no commits of its own stops right there, before anything is pushed and before CI spends a minute:
The two progress lines about reading the repos are left out above. Here the commit was made, but not yet pushed to the shadow repo. NOTHING_TO_PUBLISH exits with code 1, and error.details.reason says why:
no_commits: the feature has no commits of its own yet. Commit, push to the shadow repo, then gate.all_stripped: it has commits, but nothing is left once the overlay’s paths and internal sections are taken out. Nothing of it would go public.
CI parity
Section titled “CI parity”Your own workflows run on the candidate, in the shadow repo: the same files a publish would send, with the same workflow files. So a green gate means your public CI will very likely pass too.
The candidate starts your workflows as any branch and pull request would. Private workflows that should run only in the shadow repo go on the overlay branch, as .github/workflows/ghostbranch-*.yml.
The gate fails with exit code 3 when CI fails (CHECKS_FAILED), when no check appears at all (NO_CI), or when CI runs too long (CI_TIMEOUT). The timeouts are in .ghostbranch.yml: gate.ci_appear_timeout (10 minutes by default) and gate.ci_timeout (60 minutes).
NO_CI says why in error.details.reason: no_workflow when the candidate has no workflow under .github/workflows at all, and actions_disabled_or_untriggered when Actions is off in the shadow repo or no workflow runs on pull_request. Common problems has the fix for each.
In a stack, a passing gate’s next looks at the levels around it: it says to gate the level below first if that one’s gate isn’t fresh, then any level above that has gone stale, and only then to promote the whole stack.
Repo guards
Section titled “Repo guards”Many workflows have jobs that only run in the real repo: if: github.repository == 'you/project'. In the shadow repo those jobs would skip, and CI would test less than it should. So the gate rewrites such guards in the candidate only, and those jobs run in the shadow repo too. What gets published is never rewritten.
A guard written in a form the gate can’t rewrite safely, such as startsWith(github.repository, …), warns REPO_GUARD_UNREWRITABLE: that job skipped, and wasn’t tested. ghostbranch audit lists every guard it finds.
The leak scan
Section titled “The leak scan”The scan reads everything a publish would send:
- Secrets: API keys, tokens, private keys and the like, found by the built-in rules (
leak_scan.secrets: true). - Your own patterns: regular expressions you list under
leak_scan.block_patterns, for internal hostnames or project code names. - Internal markers that can’t be stripped safely (
MARKER_INVALID).
It reads every file, including binary files through their readable text, and in the default preserve mode it reads every commit and its message, and also its author unless authors are published as your noreply identity. So a secret added in one commit and deleted in the next still blocks: the first commit would publish it. When you publish, promote scans the branch name and the pull request’s text too.
Here a feature added a (fake) GitHub token:
The finding names the rule, the file, the line and the commit, and never the value. With --json, each finding is in error.details.findings:
{ "id": "f-34b03e644091", "rule": "github-pat", "where": "tree", "path": "drill/fake-token.txt", "line": 1, "secret": "[REDACTED]", "waivable": true, "commits": ["aa9a2bafc2eea9061a3853ed89dedf40ed791dba"]}Fixing a finding
Section titled “Fixing a finding”Remove the content from the feature and gate again. As next says, a new commit that deletes it isn’t enough in preserve mode, because the older commit would still publish it. After a fix commit, the same gate still fails, and next gives the ways out:
So you can rewrite the commit that added it and force-push the branch to the shadow repo, or publish in a history mode that never ships the older commit.
Waivers
Section titled “Waivers”Sometimes a finding is a false positive, such as a fake key in a test fixture. Only a person can waive it, with waivers add and a one-time code at their own terminal. The code is there so a coding agent can’t waive its own findings.
Without --plan, it scans the feature again on your machine, shows each finding, and asks for the code. A waiver covers exactly that content in that file: if the content changes, it’s a new finding. Waivers live on the machine that made them. waivers list shows them and waivers revoke takes one back.
A finding in a commit message, a branch name or the pull request’s text can never be waived. Change the text instead.
Warnings that don’t block
Section titled “Warnings that don’t block”Some things are worth a look before you publish, but aren’t leaks. The gate passes and warns:
BINARY_FILE: new or changed binary files would be published. They were scanned only through their readable text.LARGE_FILE: files larger thanleak_scan.large_file(5 MB by default).WORKFLOW_CHANGED: workflow files under.github/workflowsare new or changed.
Internal sections
Section titled “Internal sections”You can keep notes in a file that go no further than the shadow repo. Lines between a BEGIN-INTERNAL marker line and an END-INTERNAL marker line are removed from every published file, and CI runs on the file without them. Each marker goes on a line of its own, in the file’s comment syntax: for example // BEGIN-INTERNAL in JavaScript, or an HTML comment holding END-INTERNAL in Markdown.
A line that looks like a marker but isn’t exactly one, such as a marker followed by more words, fails the gate with MARKER_INVALID rather than guess. Never delete only the marker lines: the lines between them would then be published.
Exit code 3
Section titled “Exit code 3”Exit code 3 always means the gate said no: CI failed or didn’t run, the leak scan found something, or a marker was malformed. Nothing was published. Fix the feature, push it to the shadow repo, and gate again.
Errors you may see
Section titled “Errors you may see”LEAK_FOUNDexit 3The leak scan found something in what would be published.
Remove the content in details.findings from the feature and gate again. A finding whose commits are listed may be only in older commits: publish with --mode squash or --mode curated so they never ship, or rewrite those commits on the feature branch, force-push it to the shadow repo, and gate again. If a person judges a finding in details.waivable a false positive, they run the waivers add command in next. Agents never waive findings or edit waivers.json.
MARKER_INVALIDexit 3A file has BEGIN-INTERNAL/END-INTERNAL markers that cannot be stripped safely.
- unmatched_begin: a begin marker with no end
- unmatched_end: an end marker with no begin
- nested: a begin marker inside an open section
- malformed: a line that looks like a marker but is not exactly one (more text, a doubled prefix, or a multi-line comment)
- unsupported_encoding: markers in a UTF-16 or UTF-32 file, or in a file read as binary
Fix the marker lines at details.problems on the feature branch, push it to the shadow repo, and gate again. When details.commit names an older commit and the final tree no longer has the problem, publish with --mode squash or --mode curated so it never ships, or rewrite that commit on the feature branch and force-push it to the shadow repo. Never delete only the marker lines: the internal lines would then be published.
CHECKS_FAILEDexit 3CI failed on the gate candidate.
Fix the failing checks listed in details.failed, push to the feature branch, and gate again.
NO_CIexit 3No CI checks appeared on the gate candidate (error.details.reason).
- no_workflow: the candidate has no workflow under .github/workflows, and no CI app posts checks
- actions_disabled_or_untriggered: Actions is disabled in the shadow repo, or no workflow triggers on pull_request
- no reason: the gate candidate couldn't be read, so which of the two holds is unknown
For no_workflow, add CI to the public repo, or, if CI is an external app, install it on the shadow repo. Otherwise enable Actions in the shadow repo or add a pull_request trigger. Then gate again.
CI_TIMEOUTexit 3CI did not finish in time.
Gate again, or raise gate.ci_timeout in .ghostbranch.yml.
REBASE_CONFLICTexit 4The feature does not rebase cleanly onto public main.
Rebase the feature branch onto public main yourself, resolve the files in details.files (the commit in details.commit stopped), push, and gate again.
BINARY_FILEwarningNew or changed binary files would be published.
Check that the files in details.paths are meant to go public; they were scanned only through their printable text.
LARGE_FILEwarningNew or changed files are larger than leak_scan.large_file.
Check that the files in details.paths are meant to go public.
WORKFLOW_CHANGEDwarningWorkflow files under .github/workflows are new or changed in what would be published.
A person checks the workflow changes in details.paths before approving the publish.
REPO_GUARD_UNREWRITABLEwarningA workflow guard mentions the public repo in a form the gate can't rewrite safely, so that job skips in the shadow repo and CI does not test it.
- startsWith, contains, fromJSON or format around github.repository
- a YAML form the rewrite can't re-emit safely, such as an anchor or a multi-line plain value
A person widens the guard in the public workflow to also allow the shadow repo, for example github.repository == 'org/project' || github.repository == 'org/project-shadow'. That writes the shadow repo's name into the public repo. Never widen the guard of a job that deploys or publishes: it would run in the shadow repo on unreleased code, and its secrets must never be added there; repos whose guarded jobs deploy should use ci.rewrite_repo_guards: warn. Guards in run: scripts, reusable workflows called by repo name and composite actions are never detected.
CHECKS_LIMITEDwarningThe token cannot read check runs, so only GitHub Actions workflows and commit statuses were considered.
Use a token that can read checks if you rely on CI apps other than Actions.