Git as the Safety Net: How We Built Version Control into TransistorKit
AI can write a lot of code very quickly. That is useful right up until it is terrifying.
When several builders are changing a project, the important question is not only, “Can they ship the feature?” It is also, “Can I see what changed, try it without risking the working version, and get back to safety if the result is wrong?”
That is why Git in TransistorKit is not a checkbox integration or a button that runs git push. We built it into the project lifecycle. Local checkpoints, branches, rollbacks, GitHub remotes, pull requests, and merge controls all work together as the safety system beneath the agent fleet.
The goal was simple: give people the protection of a disciplined Git workflow without requiring them to become Git experts first.
Start local, without making Git feel mandatory
Every TransistorKit project can work locally. If a folder is not already a Git repository, TransistorKit can initialize it, add a sensible .gitignore, and create a baseline commit.
We deliberately use the real Git command-line tool rather than inventing our own repository format. A project created in TransistorKit is still an ordinary Git repository. You can open it in another editor, use Git from Terminal, or move it to another service without converting anything.
If the user already has a Git identity configured, commits use it. On a fresh Mac without one, TransistorKit uses a clearly labeled fallback identity so checkpoints still work instead of failing at the first commit.
The generated ignore rules cover common build outputs from Xcode, Swift Package Manager, Gradle, Node, CocoaPods, and the other toolchains TransistorKit supports. More importantly, they include defensive patterns for private keys, signing credentials, service-account files, keystores, environment files, and tokens. Existing .gitignore files are preserved and topped up only with missing secret protections.
That distinction matters. Build artifacts are annoying in a repository; committed credentials are an incident.
Keeping keys out of the repository
We treat secret protection as part of version control, not as a separate security feature that users must remember to run later.
The first defense is prevention. When TransistorKit initializes a repository, its .gitignore includes patterns for Apple .p8, .p12, and provisioning files; Android keystores; private SSH keys; cloud service-account files; .env variants; and common token files. If a project already has a .gitignore, we do not replace it. We compare its rules and append only the missing secret patterns.
Ignore rules are useful, but they are not proof that a workspace is clean. A key may predate the rules, use an unexpected location, or already be staged. TransistorKit therefore includes a workspace Secret Sweep that looks for credential-bearing .env files and recognizable key material such as .p8, .p12, .mobileprovision, .keystore, and .jks files.
Each finding is checked against Git and labeled honestly:
- Ignored means Git is currently configured to leave it alone.
- Untracked means the file is loose in the project but has not entered the index.
- Staged means its contents are queued for the next commit.
- Committed means the material has already entered repository history.
That classification changes the advice. An untracked key can still be prevented from entering history. A staged key needs to come out of the index. A committed key must be treated as exposed: deleting the working copy or adding a new ignore rule does not erase it from existing commits, clones, forks, or pull-request refs. TransistorKit explicitly tells the user to rotate those credentials rather than offering the false comfort of a green “deleted” message.
Selected findings can be moved into TransistorKit’s encrypted Secrets Vault. The vault stores AES-GCM-sealed blobs outside the project, with its data key protected by the Mac’s Secure Enclave when available. Importing follows a deliberately cautious order: seal the material, read it back, verify that its fingerprint matches the original, and only then offer to delete the loose file. Deletion requires a separate confirmation. If sealing or verification fails, the original file stays exactly where it was.
After a successful import, TransistorKit checks the project’s ignore rules again so a future copy of the same kind of credential does not quietly recreate the problem.
GitHub credentials follow the same principle. Personal access tokens live in the macOS Keychain, not in project settings. For authenticated Git operations, a temporary GIT_ASKPASS helper reads the token from the environment of that one process. The credential is never embedded in an origin URL, written into .git/config, or copied into the project manifest.
No mechanism can make a committed secret unexposed. Our aim is to prevent the first commit, detect loose material early, provide a safe home outside the repository, and be blunt about rotation when prevention came too late.
Checkpoints before the agents touch anything
The first layer of protection is the checkpoint.
By default, TransistorKit commits the current project state immediately before a build begins. The commit message includes the build number and a short version of the goal, which turns the Git history into a readable timeline of work rather than a pile of anonymous snapshots.
There is also an optional checkpoint after a build finishes. The two settings serve different purposes:
- The before-build checkpoint captures the last known state before an agent starts editing.
- The after-build checkpoint captures the delivered result when the fleet reaches a terminal state.
Users can create a manual checkpoint at any time from the Checkpoints panel. That panel shows commit subjects, short hashes, relative dates, and the current revision, but it avoids exposing Git plumbing unless it is useful.
Rollback is intentionally recoverable. Before restoring an older checkpoint, TransistorKit commits pending work and tags the current state as tk-before-rollback. It then restores the selected revision. The user gets the simplicity of a Restore button without turning rollback into an irreversible gamble.
Branches are the boundary around experiments
Checkpoints answer, “Can I go back?” Branches answer, “Can I try this without disturbing the version I trust?”
Projects expose their branches directly in the sidebar and in the Checkpoints panel. From there, a user can create a branch, switch branches, merge one into the current branch, or safely delete a merged branch.
Before TransistorKit creates or switches a branch, it checkpoints pending edits. A dirty worktree should never become a reason to lose work or strand someone halfway through an operation. User-entered branch names are also sanitized into valid Git ref names.
The Orchestrator uses the same workflow for larger features. It can create a branch such as feature/dark-mode, let builders commit their work there, and leave main untouched while the user tries the result. TransistorKit only merges that feature when the user has seen it and explicitly says to keep it.
We added an important safety gate for active builds: branch-changing operations are disabled while agents are running on that project. Switching the working tree out from underneath a builder is the Git equivalent of moving the stage while the hula is still in progress. The app explains why the controls are unavailable and unlocks them when the agents are finished.
We also bind every branch operation to the project that issued it—not merely whichever project happens to be selected in the interface at that moment. That prevents a background project’s request from creating a branch in the foreground project’s repository.
Merges should fail cleanly
A merge in TransistorKit starts with another checkpoint and uses a non-fast-forward merge so the feature boundary remains visible in history.
If Git reports conflicts, TransistorKit gathers the affected paths, aborts the merge, and returns the worktree to a clean state. The interface reports which files clashed instead of leaving the repository in a half-merged state that the user must diagnose.
Branch deletion uses Git’s safe -d behavior, so an unmerged branch is not discarded accidentally. For the Orchestrator workflow, deleting the feature branch after a successful merge is optional.
The principle is consistent throughout the app: automate the routine operation, preserve the escape hatch, and stop cleanly when human judgment is needed.
From a local repository to GitHub
Local Git is useful on its own, so connecting GitHub is optional and account-wide.
TransistorKit supports two authentication paths:
- The GitHub CLI, using an existing
gh auth loginsession. - A personal access token stored in the macOS Keychain.
In automatic mode, the app prefers an authenticated GitHub CLI and falls back to the Keychain token. Both paths feed the same internal models, so the repository and pull-request interface does not care which authentication method is active.
Token-based Git operations use a temporary GIT_ASKPASS helper. The helper receives the credential through the environment of that single process; the token is never embedded in the remote URL or written into the repository. The persisted project record stores the repository owner, name, visibility, default branch, and ordinary HTTPS remote—not the credential.
From a project, the user can:
- Create a new GitHub repository, private by default.
- Link an existing
owner/repositoryor GitHub URL. - Clone an existing repository into a new TransistorKit project.
- Keep the project local and publish it later.
- Open the linked repository in a browser.
New projects can also create and connect a private GitHub repository automatically when the account setting is enabled. Public repositories can be cloned anonymously when no GitHub account is connected.
Creating a repository and pushing its first branch are treated as separate outcomes. If GitHub successfully creates the remote but the first push hits a transient error, TransistorKit keeps the valid repository connection and tells the user to retry the push. It does not pretend the whole operation failed and leave an existing repository looking like an orphan.
Commit and push are separate on purpose
The project toolbar exposes local commit and remote push as separate actions.
Commit creates a local checkpoint. Push sends the current branch to origin and establishes its upstream on the first push. A live sync snapshot tracks the current branch, the default branch, and the number of commits that have not reached the remote, so the interface can say whether there is actually anything to publish.
TransistorKit never force-pushes. A rejected push is surfaced as a rejected push, with a chance to resolve the remote history rather than overwrite it automatically.
Separating the actions was a small but important design choice. A local commit is a safety action; a push is a publishing action. They should not become the same gesture merely because they often happen next to each other.
Pull requests without leaving the project
Once a branch is ready, TransistorKit can open a pull request from inside the app.
The pull-request sheet lets the user choose the base and head branches, edit a title and description, and mark the request as a draft. The title is prefilled from the branch’s latest commit. If the head branch has not been published yet, creating the pull request publishes it first.
The UI catches two common dead ends before talking to GitHub: trying to open a pull request from a branch into itself, and trying to open one when the head has no commits ahead of the base.
TransistorKit can then list pull requests and show their state, review decision, mergeability, changed-file totals, and check results. Merging supports GitHub’s merge, squash, and rebase methods, with optional remote branch deletion.
Because merging changes shared remote state, it requires explicit user intent. The Orchestrator can prepare the branch, push it, and open the pull request, but it does not decide on its own that the review is over.
Git is available to the Orchestrator and the builders
The same capabilities are exposed as structured actions to the Orchestrator and cloud builders. That means a user can say, “Put this feature on a branch, push it, and open a pull request,” without TransistorKit teaching an agent to scrape buttons or improvise shell commands.
The available operations cover the complete path:
branch → build → checkpoint → push → pull request → review → merge
The actions still obey the product’s safety rules. Creating a public repository, merging a branch, or merging a pull request requires the user’s approval. Remote pushes never force. Project roots are resolved explicitly. Failures come back as visible results in the conversation.
This is the part that makes Git feel native to TransistorKit rather than bolted on: version-control operations are part of the same conversation that started the build.
Nightshift gets its own branch, too
TransistorKit’s unattended Nightshift workflow always creates or reuses a dated branch such as nightshift/2026-07-31. It refuses to commit Nightshift results directly to main or master.
When the run finishes, the app records a single checkpoint and produces a morning report with the branch, commit count, and diff statistics. The user reviews that branch in daylight and decides whether to merge it.
Autonomous work is exactly where isolation matters most. More automation should mean stronger boundaries, not fewer of them.
The invisible engineering mattered
Git commands are blocking processes. Running them on the main thread can freeze a desktop app, especially when authentication, network access, or a large repository is involved. TransistorKit funnels Git and GitHub work onto background tasks and publishes only the resulting state back to the interface.
We also serialize process launches. Foundation’s process handling can race when multiple Git commands finish at nearly the same moment, which becomes realistic when an agent build, a branch refresh, and a GitHub operation all overlap. One launch lock keeps that plumbing boring—and boring plumbing is good plumbing.
The implementation handles missing Command Line Tools without triggering a surprise installer dialog on the app’s launch path, drains process output so a noisy command cannot deadlock on a full pipe, and normalizes GitHub CLI and REST responses into the same application models.
None of those details make a good toolbar screenshot. All of them make the toolbar trustworthy.
What we learned
The useful part of Git integration was not exposing every Git command. It was deciding where safety should happen automatically and where the user should remain in control.
Our working rules became:
- Snapshot before risk.
- Put experiments on branches.
- Do not mutate a branch beneath a running agent.
- Abort conflicts cleanly.
- Keep local commits distinct from remote publishing.
- Never put credentials in repository state.
- Never force-push on the user’s behalf.
- Require explicit approval for consequential merges.
- Keep everything compatible with ordinary Git outside the app.
AI changes the speed of software development, but it does not repeal the need for history, review, and reversibility. If anything, faster code generation makes those things more important.
In TransistorKit, Git is not where the work goes after the agents are done. It is the safety net that makes letting them work possible in the first place.