a memory for your working tree
Automatic snapshots of your uncommitted work — untracked files included —
stored as ordinary git objects. No daemon, no new VCS, no workflow change.
$ rm -rf src/parser # oops. uncommitted changes and untracked files, gone.
$ jog log # ...but every command boundary was snapshotted
c0ffee1 deadbee 2 minutes ago pre: git status
a1b2c3d deadbee 9 minutes ago claude[b3f1a2c4]: Bash(go test ./...)
9e8d7c6 deadbee 14 minutes ago manual: before parser rewrite
$ jog restore --all # worktree back to the newest snapshot
restored to c0ffee1 (2 minutes ago — pre: git status): 14 restored, 0 deleted
(undo: jog restore --all)- Why jog
- Install
- Recovery cookbook
- Usage
- Hooks
- What jog will never touch
- Why not jog
- Configuration
- How it compares
Think of jog as an insurance policy for your working tree: invisible until you need it, invaluable when you do.
Inspired largely by jj, jog is a snapshotting tool for uncommitted changes in Git working trees. It's designed to run silently inside the workflow you already have, capturing changes as they happen. With this, any mistake against the working tree becomes instantly recoverable.
Git protects your commits, but it has never protected your working tree. Losing uncommitted work to a wrong command is never fun, and now coding agents have made it commonplace. Jog aims to solve this.
- Snapshots are plain git blobs/trees/commits in your repo's own object
database, on refs under
refs/jog/<branch>— invisible to branches, the index,git log, teammates, and remotes. Unchanged files cost zero bytes. There is no jog database. - No daemon, no filesystem watcher, no git hooks to install or break. jog aims for robust over perfect: you don't need high-fidelity changesets of everything you type — you need a place to restore to when a mistake is made.
- Designed to be totally invisible. jog piggybacks the workflow you already have, snapshotting as you work. Every git repo is covered automatically with nothing to enable, and it maintains itself, so there is nothing to clean up. If you use agents, you don't even need to remember jog exists — when a mistake is made, the agent will.
# linux / macOS — verifies checksums, lands in ~/.local/bin
curl -fsSL https://raw.githubusercontent.com/tyler-johnson/jog/main/install.sh | sh# Windows (PowerShell) — installs and adds jog to your user PATH
irm https://raw.githubusercontent.com/tyler-johnson/jog/main/install.ps1 | iexThe script ends by launching jog install — the guided setup described
below.
Script installs also keep themselves up to date: jog checks for new
releases daily in the background and installs them silently — install
once and forget it. jog config autoUpdate false prints a one-line
notice instead of installing, and jog config updateCheck false opts
out of all of it.
# Homebrew (macOS / Linux)
brew install tyler-johnson/tap/jog
# or with Go 1.26+
go install github.com/tyler-johnson/jog/cmd/jog@latestPrebuilt binaries for linux/darwin/windows (amd64/arm64) are on the
releases page. Brew and
go installs update through their own tools (brew upgrade jog /
go install …@latest) — jog prints a one-line notice when a new release
exists and says which command applies.
However jog got onto the machine, finish by running jog install — a
few questions that wire up your shell, agents, and editors (the
Hooks section has the full picture). Re-running it is always
safe, and jog uninstall reverses it. Flags answer the questions for
dotfiles, CI, or a coding agent doing the setup for you:
jog install # guided setup
jog install --yes # every default, no questions
jog install --yes --preexec --agents claude # scoped; see `jog help install`Make any change in a repo, run git status, then bare jog — the
timeline it prints should show a pre: git status entry: the alias is
snapshotting. jog doctor checks all the wiring and every invariant
the engine depends on.
$ git status # through the alias — snapshots first
$ jog
no changes since the last snapshot on main
c0ffee1 deadbee 9 seconds ago pre: git statusjog log browses this branch's timeline. In a terminal it's an
interactive browser — scrub with a live diff preview to find the
version where things still worked, and press r to restore right
there. Each snapshot names the commit it was based on (the dim second
column), and ● rows mark where a commit, rebase, or reset moved
HEAD — the same short ids you see in git log, so the two timelines
cross-reference.
$ jog log
c0ffee1 deadbee 2 minutes ago pre: git status
a1b2c3d deadbee 9 minutes ago claude[b3f1a2c4]: Bash(go test ./...)
● deadbee commit: parser groundwork — 12 minutes ago
9e8d7c6 4f5e6d7 14 minutes ago manual: before parser rewritejog since answers "what did I actually change?" — a per-file summary
since the last command boundary, or any time you give it:
$ jog since 1h # everything changed since the snapshot nearest 1h ago
since 4d5e6f7 (1 hour ago — pre: git checkout main)
src/parser.go | 41 ++++++++---
src/parser_test.go (new) | 66 ++++++++++++++++++++
2 files changed, 96 insertions(+), 11 deletions(-)If the work was on another branch or in a worktree, jog branches
shows every branch's chain — deleted branches included — and
jog log --all interleaves every chain's timeline:
jog branches
jog log --allPoint jog restore at what you found: paths for the files to bring
back, and --at with a snapshot id, a time, or a position. --all
restores the whole tree instead, including deleting files created
since:
$ jog restore src/parser.go --at a1b2c3d
restored src/parser.go from a1b2c3d (9 minutes ago — claude[b3f1a2c4]: Bash(go test ./...))
$ jog restore src/config.yaml --at 9m
restored src/config.yaml from 2c4d6e8 (9 minutes ago — claude[b3f1a2c4]: prompt "clean up the src tree")
$ jog restore --all --at 25m
restored to 9e8d7c6 (26 minutes ago — manual: before parser rewrite): 11 restored, 3 deleted
(undo: jog restore --all)Every restore snapshots first, so undo is itself undoable.
Bare jog is a deliberate checkpoint: snapshot now, then show the top
of the timeline. jog -m "msg" labels it.
jog # snapshot now, show the top of the timeline
jog -m "before parser rewrite" # labeled: manual: before parser rewriteBrowse the timeline of snapshots on this branch. In a terminal it's an
interactive browser — scrub with a live diff preview, press r to
restore after a y/n confirm. Piped, it prints plainly: id, the commit
the snapshot was based on, age, provenance, files changed. Rows marked
● are commits interleaved where they happened — commit, rebase, and
reset boundaries named with the same ids as git log. With paths,
browsing and restoring are scoped to those paths. (--json carries
the base commit as base; -p, --format, and --all output are
unchanged.)
jog log # this branch's timeline
jog log src/parser.go # scoped: only snapshots touching these paths
jog log -p -n 5 # the newest 5, with full patches
jog log --all # every branch's chain, interleaved
jog log --json # machine-readable
jog log --format='%h %s' # any git log format
jog snaps # snaps and pick are aliases of logOne row per branch's snapshot chain — count, newest snapshot, and
whether the branch still exists. Deleted branches' chains stay listed
until their snapshots age out, so you can see what jog trim --gone
would drop before it drops it.
$ jog branches
* main 47 snapshots, newest 2m ago — pre: git status
feature 12 snapshots, newest 3d ago — manual: wip on parser
- old-work 5 snapshots, newest 34d ago — manual: spike
- Deleted branch, clean up with jog trim --gone
$ jog branches --json # machine-readable; jog branch is an aliasWhat changed since a snapshot — per-file summary, -p for patches.
With no target it diffs against your last command boundary; give it a
time or position (jog since 1h) to widen the window, and paths to
narrow it.
jog since # since the last command boundary
jog since 1h # since the snapshot nearest an hour ago
jog since -p 30m # patches instead of the per-file summary
jog since '@{3}' src/ # three snapshots back, scoped to src/Restore files — or with --all, the whole tree, including deleting
files created since — from a snapshot. Worktree only: your index,
branches, and HEAD never move.
jog restore src/parser.go # one file, from the newest snapshot
jog restore src/parser.go --at 1h # …as it was an hour ago
jog restore --all --at '@{2}' # whole tree, two snapshots back
jog restore --all # back to the newest snapshot (the undo)
jog back src/parser.go # back is an alias of restoreEvery restore snapshots first, so undo is itself undoable.
--at (and since's target slot) accepts a snap id from jog log, a
time, or a position on the timeline:
- a time:
--at 30m,--at 1h,--at 2d,--at 1w— plus anything git's date syntax accepts (--at yesterday,--at 2.hours.ago) - a position:
--at '@{1}'is one snapshot ago,--at '@{2}'two back — git's reflog syntax, counted on the snapshot timeline (keep the quotes; some shells expand braces)
Asking for a time older than the oldest snapshot falls back to the oldest, with a warning.
Pure passthrough: snapshot, then run the real git command exactly as typed — what the alias expands to. jog matches no verbs and reimplements nothing, so no git command, user alias, or future git feature can ever collide with it.
git checkout -- src/ # typed git goes through the alias = jog git checkout -- src/
jog git stash pop # …or spell the passthrough out yourselfShows everything jog wired — the shell lines, agent hooks and skills,
editor hooks — and removes all of it after one confirmation (--yes
skips it). Only jog's own marked lines and managed files are touched:
a hand-written alias, your settings, and any file carrying your edits
are left alone.
$ jog uninstall
jog uninstall — remove jog's wiring
currently wired:
alias zsh
preexec zsh
agents claude
editors vim
remove all of it? [y/N] y
removed:
✓ zsh alias ~/.zshrc
✓ zsh preexec ~/.zshrc
✓ claude hooks ~/.claude/settings.json
✓ claude skill ~/.claude/skills/jog/SKILL.md
✓ vim hook ~/.vim/plugin/jog.vim
snapshots are untouched — they live in each repo's refs/jog/*; `jog trim` prunes them.
the binary itself: rm ~/.local/bin/jogSnapshots are deliberately not part of removal — they're plain git
objects in each repo, and jog trim (or deleting refs/jog/*) is how
they go. The closing line names the right way to remove the binary for
how it was installed (rm, brew uninstall jog, …).
jog takes its snapshots from hooks. Three surfaces can carry one — your
shell, your coding agents, and your editor — each wired by its own
jog <surface> install command and covered below.
Your shell can get two independent lines, each serving a different purpose: the alias and the preexec hook.
The alias routes every git command you type through jog git, which
snapshots the working tree first and then runs real git, exactly as
typed. It only affects your interactive shell — scripts, IDEs, and CI
resolve git on PATH and get real git.
# bash / zsh (~/.bashrc / ~/.zshrc)
alias git='jog git'
# fish (~/.config/fish/config.fish)
alias git 'jog git'# PowerShell (add to your profile — `notepad $PROFILE`)
function git { jog git @args }The preexec hook covers the rest of your terminal: it snapshots before
every interactive command, so rm -rf, sed -i, and make clean
have a restore point too. Opt in with jog shell install --preexec;
it's one line calling jog shell-hook from the shell's
before-each-command mechanism:
# bash (~/.bashrc) — via PS0, bash 4.4+ (older bash ignores it harmlessly)
PS0='$(command -v jog >/dev/null && jog shell-hook --history -- "$(HISTTIMEFORMAT= builtin history 1)")'"$PS0"
# zsh (~/.zshrc)
__jog_preexec() { command -v jog >/dev/null && jog shell-hook -- "$1"; }; preexec_functions+=(__jog_preexec)
# fish (~/.config/fish/config.fish)
function __jog_preexec --on-event fish_preexec; type -q jog; and jog shell-hook -- "$argv"; end(PowerShell has no preexec mechanism — it gets the alias only.)
Agents are why jog exists twice over: they're the fastest writers your worktree has ever had, and the first to delete something you still needed. jog integrates with every supported client at two surfaces:
- Hooks — snapshot before every prompt and every mutating tool call, so each agent action has a boundary to roll back to.
- A skill — teaches the agent the recovery workflow itself: list
versions with
jog log, restore withjog restore, checkpoint withjog -mbefore risky work, and never declare uncommitted work gone without checking jog first.
| client | hooks | skill |
|---|---|---|
| Claude Code | ~/.claude/settings.json — PreToolUse + UserPromptSubmit |
~/.claude/skills/jog/ |
| Codex | ~/.codex/hooks.json — needs a one-time trust via /hooks |
~/.agents/skills/jog/ |
| Copilot CLI | ~/.copilot/settings.json |
~/.copilot/skills/jog/ |
| Cursor (IDE + CLI) | ~/.cursor/hooks.json |
~/.cursor/skills/jog/ |
| Gemini CLI | ~/.gemini/settings.json |
~/.gemini/skills/jog/ |
| OpenCode | plugin at ~/.config/opencode/plugins/jog.js |
~/.config/opencode/skills/jog/ |
Install with jog agents install and you'll get:
- snapshots the working tree before every prompt and every mutating tool call
- introduces jog to the agent once per session — one line saying snapshots are live and how to restore (on the clients that support injecting context: Claude Code, Codex, Gemini, OpenCode)
- can never block the agent: it always exits 0, and exits in milliseconds outside git repos
Agents get hooks; so do humans. jog editors install <editor> drops a
post-save hook into your editor, so every save inside a git repo becomes a
restorable snapshot — vim: save src/main.go in the timeline. This is the
one trigger that snapshots after the fact: the saved state is the
checkpoint (the pre-save state is your editor's own undo).
| editor | hook lands in | takes effect |
|---|---|---|
vim |
~/.vim/plugin/jog.vim |
new sessions |
nvim |
~/.config/nvim/plugin/jog.vim |
new sessions |
emacs |
~/.emacs.d/jog.el |
after one (load …) line in your init file |
sublime |
Packages/User/jog.py (per-OS path) |
immediately |
kakoune |
~/.config/kak/ (autoload, or sourced from kakrc) |
new sessions |
micro |
~/.config/micro/plug/jog/jog.lua |
new sessions |
vscode |
~/.vscode/extensions/ + ~/.vscode-server/extensions/ (whichever exist) |
after a full restart (remote windows: reload) |
jetbrains |
.idea/watcherTasks.xml — per project |
after a project reload; needs the File Watchers plugin |
- Your index — byte-identical across any jog operation (jog stages into
a private shadow index; all reads use
--no-optional-locks). - Your worktree — written only by an explicit
jog restore, which is itself snapshotted first. - HEAD, branches, tags, remotes — never written.
- Repo config — two
gc.refs/jog/*keys are set once, on first snapshot, so git's gc never expires jog's history; announced when it happens.
Crash at any point leaves at worst orphan objects for git's gc to sweep — jog only adds objects and moves its own refs, with compare-and-swap updates so concurrent triggers can't clobber each other.
Every snapshot is recoverable with stock git alone:
git log refs/jog/main # browse the chain
git show 'refs/jog/main@{2.hours.ago}:src/main.go' # read one file
git restore --source refs/jog/main --worktree -- path # restore one filejog does not protect against:
- GUI discards. VS Code's git panel, Fork, etc. call git via their own bindings — no trigger fires. (VS Code moves discarded files to the trash, which is some comfort.)
- Scripts and CI running real git. They bypass the alias by design.
- Ignored files. Never snapshotted, by design — jog is not a backup
system for
node_modulesor build artifacts. - Terminal work without the shell wiring installed. No trigger, no
snapshot. The alias only covers git commands;
jog shell install --preexecopts into a hook that snapshots before every interactive command (bash ≥ 4.4; PowerShell has no preexec mechanism), but a shell it was never wired into is unprotected.
Also worth knowing:
-
Preexec labels can go stale; content never does. The bash hook labels the snapshot from
history 1, so withHISTCONTROL=ignorespacea space-prefixed command gets the previous command's label — the snapshot itself is still taken causally before the command runs. Re-sourcing an rc file registers the hook twice; the second fire is a no-op. -
refs/jog/*stays private through normalpush/fetch/clone, but leaks viapush --mirror,clone --mirror, explicitrefs/*:refs/*refspecs, andgit bundle --all. Your snapshots contain your scratch work — know your mirror scripts. (That includesrefs/jog/@trash/*, which holds snapshots the lastjog trimdropped, one cycle longer.) -
Retention runs itself: a daily background trim drops snapshots older than
keep(jog config autoTrim falsemakes trim manual-only). Space cost is low either way (content-addressed, delta-compressed by repack);jog doctorshows the actual number, andmaxSizecan cap it. -
New files over 50 MiB are skipped (configurable, see below) and listed in the timeline entry.
Native git config keys — global or per-repo, no new file format.
jog config lists every setting with its current value and meaning;
jog config <key> <value> sets one (--global, --unset), with values
validated through git's own parsers:
| key | default | meaning |
|---|---|---|
jog.maxFileSize |
50m |
skip new files larger than this (0 disables the guard) |
jog.keep |
90.days |
jog trim drops snapshots older than this (never keeps everything) |
jog.maxSize |
0 (off) |
total disk budget for snapshots — jog trim drops oldest first until the estimate fits (one snapshot lenient) |
jog.autoTrim |
1.day |
how often the background trim runs, per repo — git expiry syntax (12.hours, 2.weeks), seconds (3600), or a bool (false means trim only runs by hand) |
jog.updateCheck |
1.day |
how often the background release check runs — git expiry syntax (12.hours, 2.weeks), seconds (3600), or a bool (false disables updates and notices entirely, regardless of autoUpdate) |
jog.autoUpdate |
true |
install new releases in the background; the next command runs the new version (false prints a one-line notice once per release instead; brew and source installs always get the notice) |
gc.refs/jog/*.reflogExpire |
never |
set by jog on first snapshot; keeps gc off jog's reflogs |
gc.refs/jog/*.reflogExpireUnreachable |
never |
same |
Two environment variables:
JOG_GIT— the git binary jog runs, as a name to find onPATHor a path to the executable. Default:gitfromPATH.JOG_DEBUG=1— hook diagnostics on stderr (hooks are otherwise silent by design).
- jj — the inspiration for snapshot-on-every-command, but it's a new
VCS with a workflow tax, and it's incompatible with
git worktree. jog is git, plus a memory. - dura — background daemon, hash-named branches that made recovery painful; dormant.
- gitwatch — commits your real branch on a watcher; pollutes history.
- Editor local history (VS Code, JetBrains) — blind to terminal and agent changes, short retention, not in git. jog's editor hooks put saves on the same timeline as everything else.
- Claude Code checkpoints — conversation + Edit/Write only; no bash changes, no untracked files, capped at 100 checkpoints / 30 days. jog is the other half.