Small collection of custom git subcommands that emit JSON, making git
output greppable, pipeable, and agent-friendly.
git log, git diff, and git status emit text shaped for humans. That makes
them painful to:
- Pipe into
jq,gron,fx, or any downstream JSON tool. - Feed to LLM agents that parse structured data more reliably than free-form text.
- Compare across runs or commits without brittle line-based parsing.
These commands produce stable JSON so the rest of your tooling can stop guessing.
See AGENTS.md for the full schema contracts and
docs/DEMO.md for live output from every command against a
real public repo (PoisonStack).
git clone https://github.com/jacksonfdam/agentic-gitutils
cd agentic-gitutils
./install.sh # symlinks bin/* into ~/.local/bin
# or pick a different target:
INSTALL_DIR=/usr/local/bin ./install.shThe installer also offers (interactively) to:
- add a discovery hint to
~/.claude/CLAUDE.mdso AI agents recognize these commands in every project (see Agent discovery below) - check whether a newer version is available on
origin
Skip the prompts with flags: --yes, --no-hint, --no-update-check.
Make sure the install dir is on your PATH. Any bin/git-foo then works as
git foo.
Uninstall: ./install.sh --uninstall (also removes the agent hint).
The current version is in VERSION and surfaced via:
git utils versionTo check for a newer version on origin and apply it safely:
git utils update # interactive: shows incoming commits, asks y/N
git utils update --check # just report whether an update is available
git utils update --yes # apply without prompting
git utils update --require-signed # refuse unless origin HEAD has a verified signatureTo make signature verification the default for this install:
git -C "$(git utils version >/dev/null; dirname "$(readlink -f "$(command -v git-utils)")")/.." \
config gitutils.requireSigned true
# or just: cd into the install dir and `git config gitutils.requireSigned true`Signature verification accepts either:
- a signed commit at
origin/<branch>(git verify-commit), or - a signed annotated tag pointing at that commit (
git verify-tag).
Both checks rely on your local GPG / SSH-signing keyring already containing
the public key of a signer you trust — git utils does not enforce who
signed, only that the signature is valid. Pair with gpg --list-keys (or
gpg.ssh.allowedSignersFile) for stricter trust.
Security model for update:
- Resolves the install dir from the binary's real path (no env-trust).
- Requires
originto be HTTPS, or SSH, to an allowlisted host (github.com,gitlab.com,bitbucket.org,codeberg.org). - Refuses to update if the working tree has uncommitted changes.
- Refuses to update if your local branch is ahead of
origin. - Uses
git pull --ff-onlyonly — never merges or rebases unfamiliar history. - Shows the incoming commits and asks for confirmation (skip with
--yes). - Optional
--require-signed(orgit config gitutils.requireSigned true) refuses to apply unlessorigin/<branch>has a verified signature on the commit or on an annotated tag pointing at it.
For a one-shot health check:
git utils doctor # dependencies, PATH, remote, agent hintTwo layers make these commands available and discoverable to AI agents in every project on this machine:
- PATH —
install.shsymlinks the commands into~/.local/bin, so any shell-launched agent inherits them just likegititself. - Awareness —
git utils install-agent-hintwrites a delimited block into~/.claude/CLAUDE.mdlisting the commands and pointing atAGENTS.mdfor the JSON schemas. Claude Code loads this file in every project automatically, so a new repo starts out with the agent already knowing the tools exist.
git utils install-agent-hint # default target: ~/.claude/CLAUDE.md
git utils install-agent-hint /path/to/file # custom target
git utils uninstall-agent-hint # symmetric removalThe block is wrapped in <!-- gitutils:begin --> / <!-- gitutils:end -->
markers — re-running install-agent-hint updates in place rather than
duplicating; existing file content is preserved.
git≥ 2.30 (porcelain v2 fields)jq≥ 1.6python3≥ 3.8 (used bygit-json-diffandgit-recent)bashorzsh
| Command | What it does |
|---|---|
git json-log |
git log as a JSON array of structured commits |
git json-status |
Working-tree status with branch ahead/behind, untracked, etc. |
git json-diff |
Full unified diff parsed into files/hunks/lines |
git json-diff-stat |
--numstat as JSON (added/deleted/binary/path) |
git json-branches |
Local + remote branches with metadata |
git json-blame |
Per-line authorship JSON (commit, author, date, summary) |
git json-show |
One commit fully expanded — metadata + per-file diff + stats |
git json-range |
Summary between two refs — commits, files, authors, stats |
git json-conflicts |
Currently-unmerged files with parsed conflict markers |
git recent |
Recently-touched files, with touch count + last-seen metadata |
git stats |
Repo summary (commits, branches, top authors, top files) |
git visual-diff |
Side-by-side HTML diff viewer (read-only, opens in browser) |
git tui-diff |
Side-by-side terminal diff viewer (ANSI, pipes through less) |
git utils |
Meta-command: version, update, doctor, agent-hint mgmt |
Run any command with no arguments to see the default; forward extra args to
the underlying git invocation where it makes sense.
git json-log --since=2.weeks --author=jackson \
| jq -r '.[] | "\(.abbreviated) \(.subject)"'git recent --since=1.month \
| jq '.[] | select(.touches > 1) | { path, touches, last_date }'git json-diff --cached \
| jq '.[] | { new_path, mode, hunks: (.hunks|length), lines: ([.hunks[].lines[]] | length) }'git json-diff-stat main...HEAD \
| jq '[.[] | .added] | add'git json-branches --local \
| jq '.[] | select(.behind > 0) | { name, behind, upstream }'git json-log -n 50 | gron | grep '\.author\.email' | sort -ugit stats | jq '{ commits_total, branches, top_author: .authors[0] }'git json-blame src/foo.ts \
| jq -r '.[] | "\(.line)\t\(.abbreviated)\t\(.author)\t\(.summary)"'git json-show HEAD \
| jq '{ subject, stats, files: [.files[] | { path: .new_path, mode }] }'git json-range "v1.2.0..HEAD" \
| jq '{ commits: .stats.commits, files: .stats.files_changed,
authors: [.authors[] | .name],
biggest: ([.files[] | { path, lines: (.added + .deleted) }] | sort_by(-.lines) | .[0:5]) }'git json-conflicts \
| jq '.[] | { path, n: (.conflicts | length),
first_ours: .conflicts[0].ours, first_theirs: .conflicts[0].theirs }'git visual-diff # working tree vs index
git visual-diff --cached # staged
git visual-diff main..feature # branch range
git visual-diff main -- src/foo.ts # working file vs main
git visual-diff v1.2.0..HEAD # since-tag overview
# Useful flags:
# --no-open don't launch the browser
# --print print the HTML path to stdout
# --output PATH write the HTML to PATH instead of $TMPDIRNavigate files with ←/→ or j/k; syntax highlighting via highlight.js
(loaded from CDN). The viewer is read-only — there's no UI to accept or
reject changes.
git tui-diff # working tree vs index, opens in less
git tui-diff --cached
git tui-diff main..feature
git tui-diff main -- src/foo.ts
git tui-diff v1.2.0..HEAD
# Useful flags:
# --no-pager write straight to stdout, no `less`
# --no-color suppress ANSI escapes
# --width N force a specific column widthANSI side-by-side render, automatically piped through less -RFX. Inside
less: q to quit, / to search, n / N for next / previous match.
Width auto-detected from the terminal; respects NO_COLOR=1.
bin/ # individual git-<command> executables
tests/smoke.sh # runs every command against a throwaway repo
install.sh # symlinks bin/* into $INSTALL_DIR (default ~/.local/bin)
AGENTS.md # JSON contracts for agent consumers
./tests/smoke.shCreates a temp repo, exercises every command, asserts shape with jq -e,
and cleans up.
A GitHub Action at .github/workflows/release.yml
automates versioning. On every push to main:
- Determines the bump level from conventional-commit prefixes since the
last tag:
feat!:orBREAKING CHANGE:in any commit → majorfeat(scope)?:in any commit → minor- everything else → patch
- bot's own
chore(release):commits are excluded from the analysis
- Increments
VERSION. - Commits the bump as
chore(release): bump to vX.Y.Z [skip release]. - Creates an annotated tag
vX.Y.Z. - Pushes both the commit and the tag.
- Opens a GitHub Release with auto-generated notes via
gh release create.
Recursion is prevented two ways:
paths-ignore: VERSIONon the trigger — the bot's own commit touches onlyVERSIONand never re-fires the workflow.[skip release]marker on the bot's commit message, checked by the job'sif:filter.
Skipping a release for a specific push: include [skip release] in the
commit subject or body.
Permissions: the workflow uses the default GITHUB_TOKEN with
permissions: contents: write. No external secrets needed for the basic
flow. If your main is protected by required reviews, the workflow's push
will be rejected — either exempt the github-actions[bot] user from the
protection or move the bump to a PR-based flow.
Signing tags: the default workflow creates unsigned annotated tags. To
sign them, add a GPG (or SSH-signing) key as a repo secret and add
run: git config user.signingkey … plus -s to the git tag call. Once
tags are signed, downstream users of git utils update --require-signed
will accept the release.
Inspired by:
- gron — Make JSON greppable.
- diffparser — Parse
git diffto JSON. - git log → JSON via jq — Simon Willison.
- Cali0707/git-utils — Reference custom-command repo.
- Custom git commands.
- New commands go in
bin/git-<name>, must be executable, must emit valid JSON to stdout unless explicitly text-mode. - Document the schema in AGENTS.md.
- Add a smoke check in
tests/smoke.sh.