A git graph for VS Code that is just a git graph. No AI, no telemetry, no account, no cloud. MIT licensed.
Preview. Everything documented here works and is covered by tests. What it does not do yet: stay fast on very long histories, or help you out of a conflicted merge.
Search GitHawk in the Extensions view, or:
code --install-extension kfirzvi-com.githawk
Then press Cmd+9 (Ctrl+9 on Windows and Linux) — or run
GitHawk: Open Git Graph. The graph opens in the panel at the bottom; changed
files appear in a tree in the sidebar under the GitHawk icon.
Nothing to configure to get started.
Git Graph is what most people used,
and it has been abandoned
(#913,
#838) with millions of
installs still depending on it. Its licence looks like MIT but removes the right to
publish, distribute, sublicense, and/or sell derivative works — so it is not open
source, and nobody can legally ship a maintained fork.
GitHawk is a clean-room replacement under a real MIT licence. Meanwhile the maintained alternatives keep adding AI to a tool whose entire job is drawing lines between commits.
Every colour comes from VS Code's own theme tokens, so the panel is light on a light theme and high-contrast on a high-contrast one. Only the lane colours are fixed: a lane has to keep its colour as your eye follows it down the graph, and stay distinct from the seven beside it, so they are eight hues chosen to hold their contrast against white and near-black alike.
Commits in correct topological order — a parent is never drawn above its child, even after a rebase or a cherry-pick. Lanes are reused as soon as a branch ends, so the gutter stays narrow on repositories with dozens of branches. Branches, remote branches, tags, and a detached HEAD are each labelled distinctly.
Click a commit: its full message, author, date, and hash appear on the right, and its files fill the Changes tree in the sidebar. Click a file to open it in VS Code's own diff editor.
The sidebar is surfaced the first time and then left alone — pulling focus on every click would make the graph unbrowsable. To ask for it deliberately, once it has been closed or covered, right-click a commit and choose "Show changes in the sidebar".
A row above the newest commit, whenever there is anything uncommitted, saying
what kind — 2 staged, 1 modified, 3 untracked. Click it and the Changes
tree fills with everything uncommitted, as one changeset against HEAD, and
comes to the front. The row disappears when the tree is clean, so its presence
is the answer to "is there anything here?".
Unlike a commit, clicking this row surfaces the sidebar every time. Browsing the graph is many clicks across many commits and the sidebar should not chase each one; there is only ever one of these rows, so clicking it is only ever a request to see the files.
Its marker is hollow and dashed rather than a commit dot, because it is not a
commit: nothing points at it, it has no hash, and it looks different the moment
you save a file. Nothing draws a line from it to HEAD either — the graph reads
every ref, so the topmost row is often not the commit your changes sit on, and a
line saying otherwise would be wrong more often than right.
One honest gap: untracked files are counted in the row but are not in the
changeset. git diff HEAD has nothing to compare a file git has never seen
against. GitHawk: Show Uncommitted Changes opens the same comparison without
the panel.
Set gitHawk.blame.style to column and every line carries the
date and author of the commit it came from, in a fixed-width column between the
line numbers and the code — IntelliJ's annotate, in VS Code.
Each commit gets its own colour, and the colours run in order: cool for the oldest lines in the file, warm for the newest. So a run of lines from one commit reads as a block without needing a separator, and scrolling shows the order the file was built in rather than just that several people built it.
Hover any line for the full message, the author, the date, how many lines that commit owns here — and "show in the graph", which opens the panel, selects the commit, and fills the Changes tree. That is the part no standalone blame extension can do: the graph is already here.
endOfLine is the quieter option — one label per block, at the end of its first
line, leaving your code where it is. Both blame the editor's buffer rather than
the file on disk, so annotations stay honest while you are part-way through an
edit.
Turn it on with GitHawk: Toggle Blame Annotations, the person icon in the
editor's title bar, or right-click in the editor. Off is the default, and the
toggle switches between off and the column — set gitHawk.blame.style directly
for endOfLine. Annotations appear in the diff editor
too — both sides, each blamed as of its own revision, which is the question a
diff raises.
The panel is short and splits its width three ways. Either side pane folds away with the thin handle beside it — the handle stays where it is when the pane is gone, so the way back is where the way out was — and the graph takes the space. GitHawk remembers which panes you had.
- Click a branch → "Review my work against …" — everything your branch adds relative to that one, measured from the merge base, including work you have not committed yet.
- Cmd/Ctrl-click several commits, or Shift-click for a run. Their combined changeset appears automatically — selecting is the request, there is no button. They do not have to be next to each other.
- "Compare … with …" — any two branches, tags, or commits, directly. Neither
side has to involve where you currently are, so you can sit on
mainand compare two other branches.
The tree always states how the comparison was made, because a merge-base diff, a direct diff, and a reconstruction answer different questions.
Click a branch, or right-click a commit, and you get a native VS Code menu — grouped by topic, with your keyboard shortcuts and theme, not a menu drawn inside a webview.
The branch labels drawn on the graph are the same click target as the branch list, so a branch is actionable wherever you happen to be looking at it. Both offer "Copy branch name" — a webview cannot be text-selected, and a branch name is what gets retyped into a checkout, a PR description, or a CI filter.
On a commit: create a branch or tag here, check it out, cherry-pick, revert, reset, copy the hash. On a branch: push, pull, check out, merge, rebase, rename, delete, or delete it on the remote.
Push and pull are per branch, and always there. The Sync group at the top
of a branch's menu offers both every time, with the state in the description —
3 behind, nothing to push, already up to date — rather than appearing only
when there is something to do. A branch that has never been pushed offers
"Publish" instead, which sets its upstream so the next push needs no arguments.
There is no --force anywhere, so git refusing a non-fast-forward is left to
refuse.
main is behind while you are on a feature branch? That does not need a
checkout. Pulling a branch you are not standing on writes the ref directly — your
working tree, index, and HEAD are untouched — and the menu says so. The branch list
shows ↓ behind, ↑ ahead, or gone at a glance, and
GitHawk: Update All Branches From Upstream does every eligible one at once.
Anything destructive asks first, and says what will be lost.
Stash entries appear in the graph as rows of their own, hanging off the commit the work was left on, and in the sidebar beside the branches and worktrees. All four sidebar sections — local, remote, worktrees, stashes — are always there, each folding away and remembering it, and each saying what it is for when there is nothing in it. Remote, Worktrees and Stashes carry a Manage button.
GitHawk: Manage Stashes lists the stash with what each entry says it is,
the branch it was made on, and when. An entry git named itself — WIP on main: 1234abc … — is marked (unnamed), because that wording describes the commit
the work was sitting on rather than the work.
Apply an entry from the list in one click, or open it for the rest: show what is in it in the Changes tree, apply, pop, or drop. Stashing asks for a message and whether to include untracked files, which is never the default — it is the one variant that sweeps up a scratch file you had not thought about.
Popping and dropping ask first. Both remove the entry, and a dropped entry survives only as a dangling commit.
A stash entry is a commit, so showing what is in it is the same comparison machinery as everything else — it lands in the Changes tree and opens in the diff editor.
Manage on the sidebar's Remote section, or GitHawk: Manage Remotes.
Either lists every remote with its URL, and a remote with a
separate push URL — the fork workflow, reading from upstream and writing to your own
copy — shows both, because being shown one of two is how you push somewhere you did
not mean to.
Add, rename, re-point, or remove one; fetch a single remote with pruning from the list without opening anything; or prune deleted branches without fetching. Removing a remote and pruning both ask first: they delete tracking refs, and nothing in the reflog brings those back.
Open a folder of projects and GitHawk finds the repositories inside it. The name at
the left of the toolbar switches between them; so does
GitHawk: Switch Repository. Switching moves everything with it — graph,
menus, comparisons, and the Changes tree.
Submodules, linked worktrees, and repositories nested inside a monorepo are all
found. How deep it looks is gitHawk.repositoryScanDepth.
Commit in a terminal, check out from the Source Control view, let an agent rebase in a worktree — the graph reloads on its own. GitHawk watches git's metadata, not your working tree, and waits for an operation to finish rather than redrawing on every step of a rebase. Your place in the history is kept: the row you are looking at stays where it is instead of sliding down as commits arrive above it.
A worktree is a second directory with a different branch checked out, sharing one repository. They stay niche because git's errors are opaque — it refuses a checkout because of a directory you deleted last month, and tells you only that the branch "is already used by worktree at …".
GitHawk names the rule instead:
- A branch checked out in another worktree is badged in the branch list, so you can see before clicking that a checkout would be refused. Its menu offers to open that worktree instead.
- A Worktrees section appears in the sidebar once you have more than one,
flagging
lockedandmissing. - Any free branch offers Create a worktree for …, suggesting a sibling of the repository named after the branch.
- A worktree whose directory is gone is reported as
missingwith a Prune entry — until that record is cleared, git keeps refusing its branch everywhere.
Each row has buttons to open a new VS Code window, open a terminal, or start an AI CLI there — Claude Code, Codex, Gemini CLI, and opencode by default, configurable. That is the one place AI appears in GitHawk: launching your tool in the right directory. It reads nothing and sends nothing.
| Setting | Default | What it does |
|---|---|---|
gitHawk.commitLimit |
500 |
How many commits to read. Higher shows more and loads slower. |
gitHawk.blame.style |
off |
Blame in the editor: column for IntelliJ's annotate, endOfLine for one label per block. |
gitHawk.autoRefresh |
true |
Reload the graph when the repository changes outside GitHawk. Watches git's metadata, never your working tree. |
gitHawk.repositoryScanDepth |
2 |
Directory levels below each opened folder to search for repositories. 0 searches the folders only; 2 covers a folder of projects, or a folder of buckets each holding projects. |
gitHawk.aiTools |
Claude Code, Codex, Gemini CLI, opencode | Commands offered by "Start an AI CLI here". |
| Command | |
|---|---|
GitHawk: Open Git Graph |
Cmd+9 / Ctrl+9 |
GitHawk: Refresh Git Graph |
Also rescans for new repositories |
GitHawk: Switch Repository |
|
GitHawk: Manage Worktrees |
|
GitHawk: Manage Remotes |
Add, rename, re-point, remove, fetch, prune |
GitHawk: Manage Stashes |
List, show, apply, pop, drop; stash the working tree |
GitHawk: Show Uncommitted Changes |
Everything uncommitted, against HEAD |
GitHawk: Start An AI CLI Here |
|
GitHawk: Update All Branches From Upstream |
Fast-forwards every branch that can be |
GitHawk: Show Log |
GitHawk's own output, when something goes wrong |
Honest list, in the order they are likely to annoy you:
- No row virtualisation. A large
commitLimitrenders every row; 500 is fine, 5000 is not. - A conflicting merge or rebase leaves you mid-operation. GitHawk reports git's message but offers no abort or resolution.
- One repository at a time — there is no combined view across several.
- Repositories are found by scanning on load, not watched. One cloned while the window is open needs a refresh.
Bug reports and pull requests are welcome — issues. See CONTRIBUTING.md for the architecture, how to run it locally, and how the three tiers of tests work.
MIT — see LICENSE. Actually MIT, in the sense that you may fork it, ship it, and sell it.






