Observed live: a stale plugin-cache version dir (1.0.0) coexisted with the active one (1.1.0). setup.md's 'try the hardcoded version path first' advice picked the stale one, its --install-raw-git-hook flag failed silently, and Claude went on an unscripted forensic dig (chained test&&echo, --help probing, grep/cat across both version dirs, cat'ing plugin.json, stat'ing .in_use mtimes) to figure out which version was actually active. Fix, in both plugins' commands/setup.md: - Never hardcode a version number. Always do one bounded find -maxdepth 1 -type d first; disambiguate multiple hits via the .in_use marker Claude Code itself writes, falling back to highest plugin.json version. Explicitly rule out the exploration that happened here (stat/mtime, --help, reading source, diffing versions) since setup.sh has no --help and unrecognized flags just error. - Wire up the declared but previously-unused argument-hint ([repo-name] [repo-path] / [vault-path] [mode]): if the user already supplied it on the command line, skip asking the question that would just re-collect the same info.
git-manager
Run git commands across one or more repositories — your code, an Obsidian
vault kept under version control, whatever else — without Claude Code
needing to cd into a directory or chain shell commands together. That
matters because chained commands (cd path && git ..., git add && git commit && git push) are exactly what breaks Bash permission whitelists:
either the whole chain needs separate approval every time, or — depending
on the Claude Code version — a chain can slip through on a rule that was
only meant to cover one piece of it. This plugin sidesteps the problem
instead of fighting it: there's simply never a reason to cd or chain.
What it does
All git operations go through a single script, git_cmd.sh:
git_cmd.sh <subcommand> [--repo <name>] [-- <git-args...>]
- No
cdneeded. Omit--repoand it operates on whatever git repository your current working directory is in — which, in a normal Claude Code session, is already the right repo. Use--repo <name>to target a different, pre-registered repository (e.g. your notes vault) without leaving your current directory. - No chaining needed. Each git operation is its own call to the script. Want to add, commit, and push? That's three separate calls, not one chained command — which is exactly what keeps every single one of them covered by the whitelist.
- A fixed set of allowed subcommands:
status,log,diff,show,fetch,remote,branch,checkout,add,commit,push,pull,tag. Anything else (reset,rebase,stash,merge,filter-branch, arbitrarygit config, ...) is rejected by the script itself, before git ever runs — not because those operations are inherently dangerous, but because they can rewrite or discard history/working-tree state in ways that are hard to reverse, so they should go through a normal confirmation prompt rather than being auto-approved.
Setup
After installing the plugin:
/git-manager:setup
This will:
- Ask you (optionally) to register named repositories — useful if you work with more than one repo in the same session, e.g. a code repo plus a separate Obsidian vault repo. Skip this if you're fine relying on the default (current working directory).
- Add exactly one entry to your Claude Code permissions:
Bash(<path-to-git_cmd.sh>:*)— nothing broader. - Ask whether you want an additional safety net installed: a hook that
forces a normal confirmation prompt (not a silent block, not a
silent pass-through) any time a Bash command contains shell chaining or
substitution characters (
&&,;,|,`,$(...)) — see "The anti-chaining hook" below. - Ask whether you also want a second, independent hook installed: one that
forces the same kind of normal confirmation prompt any time a Bash
command runs raw
gitinstead ofgit_cmd.sh— see "The raw-git nudge hook" below.
Usage examples
# Current repo (no --repo needed)
git_cmd.sh status
git_cmd.sh log -- --oneline -10
git_cmd.sh add -- -A
git_cmd.sh commit -- -m "Update notes"
git_cmd.sh push -- origin main
# A different, pre-registered repo — still no cd
git_cmd.sh status --repo homelab-notes
git_cmd.sh push --repo homelab-notes -- origin main
Everything after -- is passed to git as literal arguments (not
re-parsed by a shell), so quoting in commit messages etc. is safe.
Registering repositories
Repositories are looked up by name in
~/.agent-skills/git-manager/config.json:
{
"repos": [
{"name": "homelab-notes", "path": "/absolute/path/to/vault"},
{"name": "dwh-pipeline", "path": "/absolute/path/to/repo"}
]
}
This file is entirely optional. Without any entries, --repo simply isn't
available and every call operates on the current directory. Add entries any
time by asking Claude to register a new repo, or by editing the file
directly — writing to this file is never in the permissions whitelist, so
you'll always see (and can decline) that change.
The anti-chaining hook
Optional, offered during /git-manager:setup. If installed, it watches
every Bash command Claude Code is about to run (not just git ones) and,
if the command contains a chaining or substitution operator, forces the
normal permission prompt instead of letting whitelist matching decide.
It never silently blocks anything and never silently approves anything —
worst case, you get one extra confirmation prompt you didn't strictly need;
best case, it catches a chained command before it runs unreviewed.
With git_cmd.sh in place, chained commands involving git shouldn't come
up in the first place — this hook is a backstop for the rare cases they do
anyway (or for other tools/skills in your setup that aren't as careful
about it).
The raw-git nudge hook
Optional, offered during /git-manager:setup, independent of the
anti-chaining hook above (you can install neither, either, or both). If
installed, it watches every Bash command and, when it sees raw git used
instead of git_cmd.sh, forces the normal confirmation prompt instead of
letting whitelist matching decide — the same "never silently block, never
silently allow" behavior as the anti-chaining hook.
This is deliberately not a hard block. Raw git stays a legitimate
fallback for subcommands git_cmd.sh doesn't support (stash, merge,
rebase, reset, ...) and for repos that aren't registered yet — a full
block would remove that fallback entirely. What the hook does instead is
make sure raw git is never invisible: every time it's used, it's a
confirmed, visible choice, worded more insistently when the subcommand is
one git_cmd.sh already supports (in which case there's rarely a good
reason to have bypassed it) and more permissively otherwise.
What this hook can't do — and isn't trying to do — is figure out from the
surrounding conversation whether raw git was actually the right call here.
It only ever sees the literal Bash command text, not the request that led
to it; that kind of judgment is the skill's own job (see SKILL.md), and
this hook is just the backstop for when that judgment gets skipped.
What's deliberately not included
- No
Bash(git:*)— that would also permitgit reset --hard,git clean -fdx,git filter-branch, etc. - No
Bash(cd:*)— removed as a category, not just discouraged. - No auto-approved writes to the config file — registering a repo is always a visible, confirmed step.
push --forceis technically reachable (it's justpushwith extra args) since it's a normal, if consequential, part of a git workflow — it isn't specially gated beyond the normalpushapproval.