fuckgit installs as a Cargo bin literally named `git` (via `[[bin]] name = "git"`). When invoked inside a jj worktree by an AI agent, it prints the equivalent `jj` subcommand plus the live `jj help <sub>` output to stderr and exits 2 — the AI sees a clear failure and the right correction. Outside a jj repo, or with no intercept marker, it is a transparent passthrough. The intercept gate fires only when: - effective cwd (after honoring `git -C`) is inside a `.jj` worktree - FUCKGIT_INTERCEPT=1 or an AI marker (CLAUDECODE) is set - subcommand is not on the plumbing allowlist (rev-parse, cat-file, for-each-ref, ...) - GIT_DIR / GIT_EXEC_PATH / CARGO are not set - no top-level --git-dir= / --work-tree= flag is present Otherwise we walk PATH, skip self by canonicalized current_exe(), and exec() the first other `git` we find — preserving signals, exit code, and TTY ownership. Escape hatches: FUCKGIT_PASSTHROUGH=1 env var, --fucking-git flag (stripped before exec). FUCKGIT_DEPTH guard aborts at depth >= 2 to prevent self-recursion. Linux and macOS, zero runtime dependencies.
97 lines
3.1 KiB
Markdown
97 lines
3.1 KiB
Markdown
# fuckgit
|
|
|
|
A binary literally named `git` that, when run inside a [jj][] (Jujutsu) repo,
|
|
refuses to actually run git and instead tells you to use `jj`.
|
|
|
|
[jj]: https://github.com/jj-vcs/jj
|
|
|
|
## Why
|
|
|
|
AI coding agents (Claude Code, Cursor, et al.) keep running `git` inside
|
|
jj-managed repos despite being told to use `jj`. In a colocated jj+git
|
|
layout, that corrupts state. `fuckgit` shadows the real `git` on `PATH`
|
|
and traps these invocations, redirecting them with the equivalent `jj`
|
|
subcommand and inline `jj help <subcmd>` output.
|
|
|
|
It works on humans too, when they ask for it.
|
|
|
|
## Install
|
|
|
|
```sh
|
|
cargo install fuckgit
|
|
```
|
|
|
|
This installs a binary named **`git`** (yes, really) to `~/.cargo/bin/git`.
|
|
Make sure `~/.cargo/bin` is ahead of `/usr/bin` on `PATH`.
|
|
|
|
Linux and macOS only.
|
|
|
|
## Behavior
|
|
|
|
By default, `fuckgit` is a silent passthrough — every `git` invocation is
|
|
forwarded to the real `git` on `PATH`. The redirect *only* fires when
|
|
**all** of these are true:
|
|
|
|
1. You're inside a jj worktree (a `.jj` directory exists in the cwd or an
|
|
ancestor — honoring `git -C <path>`).
|
|
2. The intercept gate is open — at least one of:
|
|
- `FUCKGIT_INTERCEPT=1` is set, OR
|
|
- an AI-agent marker env var is set (currently just `CLAUDECODE`).
|
|
3. The subcommand isn't on the plumbing allowlist (`rev-parse`, `cat-file`,
|
|
`for-each-ref`, …).
|
|
4. None of the safety env vars are set: `GIT_DIR`, `GIT_EXEC_PATH`, `CARGO`.
|
|
5. No top-level `--git-dir=` or `--work-tree=` flag is present.
|
|
|
|
If any check fails, `git` behaves exactly as if `fuckgit` weren't installed.
|
|
|
|
When the redirect *does* fire, you'll see something like:
|
|
|
|
```
|
|
Don't use git. Use jj.
|
|
|
|
git status → jj st
|
|
jj has no staging area; `jj st` shows the working-copy commit's diff.
|
|
|
|
--- jj help status ---
|
|
Show high-level repo status [default alias: st]
|
|
…
|
|
|
|
Override with FUCKGIT_PASSTHROUGH=1 or git --fucking-git <args...>
|
|
```
|
|
|
|
…and exit `2`.
|
|
|
|
## Escape hatches
|
|
|
|
When you need real `git` and the intercept is firing:
|
|
|
|
- `FUCKGIT_PASSTHROUGH=1 git <args>` — env var override
|
|
- `git --fucking-git <args>` — flag override (the flag is stripped before
|
|
exec'ing real git)
|
|
|
|
## Configuration
|
|
|
|
| Variable | Effect |
|
|
|-------------------------|------------------------------------------------------------------|
|
|
| `FUCKGIT_INTERCEPT=1` | Open the intercept gate (opt in for your own jj repos). |
|
|
| `FUCKGIT_PASSTHROUGH=1` | Force passthrough even with the gate open. |
|
|
| `CLAUDECODE` | Set by Claude Code; opens the gate automatically. |
|
|
|
|
To extend the AI-marker list, edit `AI_MARKERS` in `src/main.rs` and rebuild.
|
|
|
|
## Supported subcommands
|
|
|
|
`fuckgit` knows the `jj` equivalent for these `git` subcommands:
|
|
|
|
`status`, `log`, `commit`, `add`, `checkout`, `switch`, `restore`, `branch`,
|
|
`merge`, `rebase`, `pull`, `push`, `fetch`, `clone`, `init`, `diff`, `show`,
|
|
`reset`, `revert`, `cherry-pick`, `stash`, `tag`, `blame`, `remote`,
|
|
`bisect`, `config`, `reflog`.
|
|
|
|
Unknown subcommands fall through to a generic "use jj instead" + `jj help`
|
|
output.
|
|
|
|
## License
|
|
|
|
MIT — see [LICENSE](LICENSE).
|