Added git-manager, updated READMEs

This commit is contained in:
Henner M. Kruse
2026-08-04 10:45:25 +02:00
parent c774df7ad7
commit e485d33f00
13 changed files with 830 additions and 72 deletions
@@ -0,0 +1,9 @@
{
"name": "git-manager",
"description": "Manages git operations across one or more repositories (code, Obsidian vaults, etc.) through a single whitelisted wrapper script, avoiding the cd/chaining patterns that break Claude Code's Bash permission whitelist.",
"version": "1.0.0",
"author": {
"name": "Henner"
},
"keywords": ["git", "version-control", "workflow"]
}
+122
View File
@@ -0,0 +1,122 @@
# 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`:
```bash
git_cmd.sh <subcommand> [--repo <name>] [-- <git-args...>]
```
- **No `cd` needed.** Omit `--repo` and 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`.
Anything else (`reset`, `rebase`, `filter-branch`, arbitrary `git config`,
...) is rejected by the script itself, before git ever runs — not because
those operations are inherently dangerous, but because they're
consequential enough that they should go through a normal confirmation
prompt rather than being auto-approved.
## Setup
After installing the plugin:
```
/git-manager:setup
```
This will:
1. 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).
2. Add exactly one entry to your Claude Code permissions:
`Bash(<path-to-git_cmd.sh>:*)` — nothing broader.
3. 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.
## Usage examples
```bash
# 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`:
```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).
## What's deliberately *not* included
- No `Bash(git:*)` — that would also permit `git 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 --force` is technically reachable (it's just `push` with extra
args) since it's a normal, if consequential, part of a git workflow —
it isn't specially gated beyond the normal `push` approval.
+137
View File
@@ -0,0 +1,137 @@
---
description: Set up the git-manager skill (permissions whitelist, optional anti-chaining hook)
argument-hint: [repo-name] [repo-path]
---
Set up the `git-manager` skill end to end. As part of the `git-manager`
plugin, this command is automatically namespaced by Claude Code and invoked
as `/git-manager:setup`. Follow these steps in order.
## 1. Determine the skill's install directory
This plugin's `skills/git-manager/` is a symlink into the repo's
tool-neutral `skills/git-manager/` directory (the single source of truth
for `SKILL.md` and `scripts/`, shared across all vendor adapters in this
repo). Resolve it to its real, absolute, symlink-free path:
```bash
realpath <plugin-dir>/skills/git-manager
```
This resolved path is `<SKILL_DIR>` for the rest of this setup.
## 2. Optionally register named repos
If `$ARGUMENTS` contains a repo name and path, or the user wants to
pre-register repos now (e.g. an Obsidian vault repo used alongside a code
repo), write/merge them into
`~/.agent-skills/git-manager/config.json`:
```bash
mkdir -p ~/.agent-skills/git-manager
cat > ~/.agent-skills/git-manager/config.json << 'EOF'
{
"repos": [
{"name": "<short-name>", "path": "<absolute-repo-path>"}
]
}
EOF
```
If the file already exists, merge new entries in rather than overwriting.
This step is entirely optional — the skill works without any named repos,
operating on the current working directory by default.
## 3. Generate and merge the minimal permissions whitelist
Exactly one command needs whitelisting — everything else about this skill's
safety comes from the wrapper script's own subcommand allowlist and repo
resolution, not from a long list of Bash rules:
- `Bash(<SKILL_DIR>/scripts/git_cmd.sh:*)`
- `Read(~/.agent-skills/git-manager/config.json)`
Steps:
1. Locate the target settings file: prefer the project-level
`.claude/settings.json` if a project is open, otherwise the user-level
`~/.claude/settings.json`. Ask the user which one they want if unclear —
note that if they work across many separate git repos/projects, the
user-level file avoids repeating this setup per project.
2. If the target file doesn't exist yet, create it containing just these
two entries under `permissions.allow`.
3. If it exists, read it first and merge: add only entries not already
present. Don't duplicate, don't remove or overwrite unrelated existing
permissions.
4. Show the user exactly what was added before writing.
Do **not** whitelist a generic `Bash(git:*)` or anything targeting `cd`.
Do **not** whitelist writing to the config file — that keeps prompting for
confirmation, so registering a new repo is always a visible, confirmed
action.
## 4. Offer the anti-chaining hook
Explain to the user: this hook never silently blocks and never silently
allows anything. It only ever forces the *normal* confirmation prompt
(`permissionDecision: "ask"`) when a Bash command contains shell chaining
or substitution operators (`&&`, `;`, `|`, backticks, `$(...)`). This exists
because Claude Code's own Bash allow-list matching has had bugs where
compound commands either bypass per-command checks entirely, or where a
chain of individually-allowed commands still isn't recognized as such — see
`hooks/force-ask-on-chaining.sh` for the reasoning and a link to the
relevant upstream issue. With this hook installed, if a chained command
ever gets constructed anyway (it shouldn't, since `git_cmd.sh` removes the
need for `cd`/chaining), the user will always see a normal approval prompt
for it rather than it silently going through or silently failing.
Ask the user whether to install it. If yes:
1. Locate `hooks/force-ask-on-chaining.sh` next to this plugin's
`plugin.json` and note its absolute path, `<HOOK_PATH>`.
2. Ensure it's executable: `chmod +x <HOOK_PATH>`.
3. Merge this into the same settings file chosen in step 3 (project or
user-level — ask if unclear, but note this hook is most useful applied
broadly at the user level, since it's about Bash hygiene in general, not
specific to git):
```json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "<HOOK_PATH>"
}
]
}
]
}
}
```
Merge into any existing `hooks.PreToolUse` array rather than overwriting it
— don't remove other existing PreToolUse hooks for the `Bash` matcher or
other matchers.
If the user declines, skip this step entirely; the whitelist from step 3
still works on its own, it just doesn't have this extra safety net.
## 5. Make the wrapper script executable
```bash
chmod +x <SKILL_DIR>/scripts/git_cmd.sh
```
## 6. Confirm and offer a test run
Summarize to the user:
- Any repos registered in step 2
- The exact permission entries added, and where
- Whether the anti-chaining hook was installed, and where
Then offer to run `<SKILL_DIR>/scripts/git_cmd.sh status` (against the
current directory, or a registered repo) as a quick sanity check.
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env bash
# PreToolUse hook for the Bash tool. Does NOT silently block anything —
# per design, it only ever forces the normal confirmation prompt
# (permissionDecision: "ask") when a Bash command contains shell chaining
# operators. It never returns "deny" and never returns "allow" itself; it
# either stays out of the way (no JSON output) or asks. This exists as a
# safety net against Claude Code's Bash allow-list not always splitting
# compound commands the way the docs describe (see e.g.
# https://github.com/anthropics/claude-code/issues/20085), so a chained
# command can't slip through on a whitelisted prefix without the user
# seeing it.
#
# Register in settings.json under hooks.PreToolUse with a matcher of "Bash".
set -euo pipefail
INPUT="$(cat)"
if ! command -v jq >/dev/null 2>&1; then
# No jq available — fail open (no output = no opinion), rather than
# breaking every Bash call because a dependency is missing.
exit 0
fi
TOOL_NAME="$(echo "$INPUT" | jq -r '.tool_name // empty')"
if [ "$TOOL_NAME" != "Bash" ]; then
exit 0
fi
CMD="$(echo "$INPUT" | jq -r '.tool_input.command // empty')"
# Look for shell chaining/substitution operators anywhere in the command.
# Deliberately broad (better a false positive prompt than a missed chain):
# && || ; | backticks $( )
if echo "$CMD" | grep -qE '[;&|`]|\$\('; then
jq -n \
--arg reason "Command appears to chain multiple shell commands (&&, ;, |, or command substitution). Forcing a normal confirmation prompt instead of relying on the allow-list, since compound commands can bypass per-command whitelisting." \
'{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "ask",
permissionDecisionReason: $reason
}
}'
fi
exit 0
@@ -0,0 +1,14 @@
{
"_comment": "Template for the git-manager skill's Claude Code permissions. <SKILL_DIR> must be replaced with the absolute, symlink-resolved path of skills/git-manager (see commands/setup.md step 1) before merging into settings.json. Exactly one Bash rule is whitelisted — the wrapper script itself — never a generic Bash(git:*) or anything targeting cd. The wrapper script enforces its own subcommand allowlist (status, log, diff, show, fetch, remote, branch, checkout, add, commit, push, pull) and resolves repo paths only from ~/.agent-skills/git-manager/config.json or the current working directory, never from an arbitrary caller-supplied path.",
"permissions": {
"allow": [
"Bash(<SKILL_DIR>/scripts/git_cmd.sh:*)",
"Read(~/.agent-skills/git-manager/config.json)"
]
},
"_deliberately_not_whitelisted": [
"No generic Bash(git:*) — that would allow any git subcommand, including destructive ones like reset --hard, clean -fdx, or filter-branch, which are not on the wrapper's allowlist for a reason.",
"No Bash(cd:*) — the wrapper's --repo flag and its default-to-$PWD behavior remove any legitimate need for cd, and cd:* is a known vector for whitelist bypass via chaining (e.g. cd X && anything).",
"No write access to the config file — registering a new named repo stays a confirmed action every time."
]
}
+1
View File
@@ -0,0 +1 @@
../../../../../skills/git-manager