A standalone CLI for managing cross-OS development workspaces: per-account git identity routing plus per-platform build-output redirection over a shared source tree.
Formerly
git-identity. Binary still installs under both names this release —git-identityprints a deprecation notice and will be removed next release.
Two related friction points if you work across multiple GitHub accounts AND across OSes (e.g. dual-boot Linux + Windows on a shared partition):
Identity side
- Wrong commit identity — pushing personal commits with your work email
- SSH key juggling — manually managing which key authenticates where
- Clone URL friction — forgetting to modify clone URLs for the correct SSH alias
Workspace side
- Build output collisions —
target/,node_modules/,.venv/written into a source tree shared between Windows and Linux: slow I/O on Linux over NTFS, broken exec bits, file-lock collisions, double rebuilds - Setup drift — each teammate (or each OS, on a dual-boot machine) wires things slightly differently
Git's includeIf gitdir: + SSH host aliases solve the identity side. Symlinks (with directory-junction fallback on Windows) solve the workspace side. The setup for either is manual and easy to break.
graft does it all in one config: one YAML, one command, generated correctly.
You define your accounts in a single YAML config. graft apply generates:
| File | What it writes |
|---|---|
~/.gitconfig |
Default [user] identity + [includeIf "gitdir:..."] blocks (uses gitdir/i: on Windows for case-insensitive matching) |
~/.gitconfig-{alias} |
Per-account [user] override (name + email) |
~/.ssh/config |
Host github.com-{alias} blocks with IdentityFile + IdentitiesOnly |
<repo>/<link> |
Per-project symlink to the platform-native build location (Rust → target/, Node → node_modules/, Python → .venv/, etc.) |
<repo>/.git/info/exclude |
Local-only ignore entry so the link never accidentally gets committed |
Existing content in these files is preserved — graft only manages a clearly marked block:
# --- BEGIN graft managed block ---
# Do not edit this section manually. Managed by graft.
[user]
name = Jane Developer
email = jane@personal.dev
[includeIf "gitdir:D:/dev/workco/"]
path = ~/.gitconfig-workco
# --- END graft managed block ---
# ... your other git config is untouched ...cargo install --path .Or build from source:
cargo build --release
# Binary at target/release/graft (and target/release/git-identity during the deprecation window)graftOn first run, a setup wizard walks you through creating your config — base directory, account name, email, GitHub username, and whether to enable workspace links. Done in 30 seconds.
For CLI-only setup: graft init
If you said yes to workspace links during init:
graft workspace status # show what would change
graft workspace apply # create/repair links across all detected projectsgraft detects Rust / Node / Python projects by marker file at the repo root and creates a symlink (or directory junction on Windows when symlinks need elevation) named exactly what the toolchain expects. cargo build just works — its target/ lives on your platform's native filesystem instead of the shared NTFS partition.
# Work repos — use the aliased host
git clone git@github.com-workco:org/repo.git D:/dev/workco/repo
# Personal repos — default host, no alias needed
graft clone git@github.com:janedev/dotfiles.git # graft picks the right host automaticallyEach account has two short names:
- alias — drives SSH hosts and config file names:
github.com-{alias},~/.gitconfig-{alias} - dir_name — the actual folder under
base_directory:D:/dev/{dir_name}/
These can differ.
Exactly one account must have is_default: true. This account uses github.com as its SSH host (no -alias suffix), has its [user] block in the main ~/.gitconfig, and is the fallback identity for repos outside any account directory.
graft wraps generated content in sentinel comments (BEGIN/END). On apply, only the managed block is replaced — everything outside it is preserved.
Source lives on the shared filesystem; build output gets redirected:
| Platform | Default target template |
|---|---|
| Linux / macOS | ~/build/{account}/{repo} |
| Windows | C:/build/{account}/{repo} |
Detectors that fire by default:
| Marker file | Link name | Project type |
|---|---|---|
Cargo.toml |
target/ |
Rust |
package.json |
node_modules/ |
Node |
pyproject.toml |
.venv/ |
Python |
requirements.txt |
.venv/ |
Python (legacy) |
A repo can match multiple detectors and gets a link for each. Each generated link name is added to that repo's .git/info/exclude so it never accidentally gets committed.
On Windows, graft creates a real symlink if Developer Mode / admin is available; otherwise falls back to a directory junction (no elevation required, works on local volumes only — which is the supported case).
Every file write and link creation prompts for confirmation by default. Use --yes to auto-approve, or set always_allow per category in your config to stop being asked.
graft runs separately on each OS. Each side gets its own ~/.gitconfig, SSH keys, and platform-specific build target. The shared bit is the source tree itself.
| Linux / macOS | Windows | |
|---|---|---|
| Config path | ~/.config/graft/config.yaml |
%USERPROFILE%\.config\graft\config.yaml |
| gitdir keyword | gitdir: |
gitdir/i: (case-insensitive — NTFS is) |
| Symlinks | std::os::unix::fs::symlink |
Symlink → junction fallback |
Same path layout on both OSes — handy for dotfiles repos. Configs from the pre-rename location auto-migrate on first load.
For a single shared config across both partitions of a dual-boot machine
(e.g. via a dotfiles symlink), use the per-OS form of base_directory so
D:\dev resolves on Windows and /mnt/d/dev resolves on Linux. See
sample-config.yaml.
cargo build # debug build
cargo test # run tests
cargo run --bin graft -- list # test the list command
cargo run --bin graft -- apply --dry-run # preview changes
cargo clippy # lintMIT