---
title: "Work without GitHub"
description: "Run the whole ticket loop on plain local git: branch, worktree and Worker unchanged, Review as a local diff, Ship as a local squash-merge."
url: "https://www.fredrin.com/docs/local-git"
updated: "2026-09-12"
---

# Work without GitHub

Fredrin's ticket loop is git-shaped, not GitHub-shaped. Local git mode runs all of it against a repository on your machine, and connecting GitHub later is an upgrade rather than a migration.

A ticket in local git mode still gets its own branch, its own [worktree](https://www.fredrin.com/docs/board/worktrees) and its own [Worker](https://www.fredrin.com/docs/workers). Only the last two steps change: Review shows a **local diff** instead of a pull request, and Ship is a **local squash-merge** instead of a GitHub merge. Merging still auto-completes the ticket and moves it to Completed. Only the signal differs.

Everything that needs a remote is what you give up: no pull request, no CI status flowing back onto the card, and no push. Those are progressive enhancements, not prerequisites.

## Starting without a GitHub account

First run asks how Fredrin should ship your work before it asks for anything else. Pick **Local git** and the GitHub App permissions step and the `gh` CLI check are removed outright rather than skipped, so neither ever runs.

After that, **Add a project** opens on **Import from GitHub**. The repo picker is the first screen for everyone, except straight after you pick **Local git** in first run, where it opens on **Start without GitHub** instead. **Continue without GitHub** reaches that screen from the picker at any time, and **Back** returns. It offers two ways to begin:

- **New project**: give the Project a name, choose where to save it, then press **Create project**. Back keeps what you entered. Fredrin creates a repository with `git init`, a `README.md`, a `.gitignore` and a stub `AGENTS.md`, then a single `chore: initial commit`.
- **Open a folder**: choose a repository folder already on this machine, optionally change its Project name, and press **Open project**. If the repository has a remote, Fredrin tells you; local mode still works on it.

**Creating a Project never writes into a folder that already holds something.** A missing folder is created, an empty one is used in place, and anything else is refused by name. Cancelling the folder picker keeps your draft. If repository initialization fails, it rolls back; if the repository was created but Project registration failed, retrying the same name and location in the open dialog reuses it.

The board opens on Tasks with zero Tickets and no Worker running. Choose **Start with kickstarts** or **Start blank** when you are ready to begin.

## Reviewing and shipping

Review is the same job it always is (read the diff and decide), except that the diff is local, computed against the base branch in your own repository.

Ship squash-merges the ticket branch onto the base branch in your real checkout. Because there is no pull request to close and no "revert this merge" button, the pre-flight is deliberately conservative: **it never stashes, resets or discards anything.** If it cannot merge cleanly it refuses and names the reason, most actionable first:

| It says | What to do |
|---|---|
| The branch does not exist | The ticket's branch was deleted; re-run the ticket |
| Nothing to merge | The Worker made no commits on that branch |
| Conflicts | Resolve them yourself, then Ship again |
| Uncommitted changes in your checkout | Commit or clean your working tree first |
| The base branch is checked out in another worktree | Close that worktree; git will not check the branch out twice |

## Connecting GitHub later

Asking for GitHub is the switch. Turn **Local git mode** off in **Settings → Git** and a **Publish to GitHub** pre-flight appears underneath it. That is the one way to promote a local project, and it is a pre-flight rather than a button: it names each check and prints the exact git it will run before you press it. Nothing has left your machine at that point; the flip only says what you want.

It tests **ancestry**, not emptiness, so an already-half-pushed repo is resumable and a repo whose history a push would destroy is not waved through:

| Destination | What happens |
|---|---|
| No commits | Push straight in |
| Remote is behind local | Fast-forward push |
| Remote is ahead of local | Pull first, so the push is not rejected |
| Diverged, but with a shared history | Merge, then push |
| Unrelated histories | **Refused.** No force, no override |

The reconcile is a merge, never a rebase: ticket branches are cut off local commits, and rewriting their SHAs would strand every in-flight ticket. A merge that conflicts is unwound and the paths are named, so the tree you come back to is the one you left.

Only the **default branch** is pushed. Ticket branches stay local, so a ticket already in Review finishes as a local diff, and tickets started after the flip open pull requests. Your GitHub account is linked to your existing workspace, never signed in as a second user.

Publishing runs on the machine holding the repository, and nowhere else.

## If you do not see these options

Local git mode is offered per platform. Creating a local Project requires your deployment to enable it and a desktop app that can create repositories and choose a destination folder. Opening an existing folder requires local git availability and the desktop folder picker.

When neither is available (a browser, or a deployment with local git off), **Continue without GitHub** is not offered at all, and GitHub import is the whole dialog. When only one of the two is available, that action goes straight to it instead of showing a chooser. An older desktop app may still open existing folders even when it needs an update to create a new repository.
