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
+103
View File
@@ -0,0 +1,103 @@
# obsidian-vault-kb
Turns an Obsidian vault — a folder of Markdown notes with frontmatter,
`#tags`, and `[[wikilinks]]` — into a knowledge base Claude can search and
reason over. Works with any vault, on any topic: homelab documentation,
project notes, research, whatever you keep in Obsidian.
## What it does
- **Reads your vault's own structure** rather than imposing one: your
folders become topical categories, your tags and frontmatter become
filters, your wikilinks let Claude follow related notes (backlinks) the
same way you would clicking through the graph.
- **Answers grounded in what's actually in your notes.** If the vault
doesn't have an answer, Claude says so instead of guessing, and keeps
general knowledge clearly separate from what came from your vault.
- **Optionally writes back**, depending on the mode you choose at setup —
from strictly read-only, to appending new dated notes, to actively
updating existing ones.
## Setup
After installing the plugin:
```
/obsidian-vault-kb:setup
```
This will ask you for:
1. **The vault path(s).** You can register more than one vault (e.g. a
personal one and a work one) and refer to them by name.
2. **A mode:**
- `read-only` — Claude can search and read, never write anything.
- `append` — Claude can also file new findings as new, dated notes.
Existing notes are never touched.
- `maintain` — Claude can also update existing notes directly (e.g. add
a dated "Update" section, change a status field).
It also whitelists three narrow, read-only helper scripts (see below) so
searching your vault doesn't require a permission prompt on every single
`find`/`grep` call.
## Usage
You don't need to invoke anything explicitly — just ask Claude things like:
- "What did I write about the Ceph cluster rebuild?"
- "Check my notes for how I set up mTLS on Postfix."
- "Any open items in my homelab notes related to DNS?"
Claude recognizes references to "my notes", "my vault", "my docs", etc.
and searches automatically. Under the hood it uses three scripts:
- `vault_index.sh` — folder structure, note counts, tags, index/MOC files
- `vault_search.sh "<query>"` — full-text search, optionally scoped to a
subfolder
- `vault_backlinks.sh "<note-name>"` — notes linking to a given note
All three resolve the vault path only from
`~/.agent-skills/obsidian-vault-kb/config.json` — they can't be pointed at
a different directory than what you configured, which is what makes it
safe to whitelist them without granting broader filesystem access.
## Writing to your vault
Only relevant in `append` or `maintain` mode:
- **`append`** — new findings are filed as new notes (e.g.
`Notes/2026-08-04-dns-fix.md`), with frontmatter and wikilinks to related
topics. Existing files are never modified.
- **`maintain`** — Claude may extend an existing note directly instead of
creating a new one, when that's the more natural place for the
information. You'll always see a summary of what's about to be written
before it happens.
- Writing into the vault is never in the permissions whitelist, regardless
of mode — every write stays a normal, visible, confirmable action.
## Multiple vaults
Registered vaults live in `~/.agent-skills/obsidian-vault-kb/config.json`:
```json
{
"vaults": [
{"name": "homelab", "path": "/absolute/path/to/homelab-vault"},
{"name": "worldbuilding", "path": "/absolute/path/to/tangier-vault"}
],
"mode": "append"
}
```
If you only ever use one vault, you don't need to name it when asking
Claude to search — it's used by default.
## What's deliberately *not* included
- No generic `Bash(find:*)`, `Bash(rg:*)`, `Bash(grep:*)`, or `Bash(cat:*)`
— those would allow reading anywhere on the filesystem, not just your
vault.
- No auto-approved writes — not to notes, not to the config file.
- Binary attachments (PDFs, images) inside the vault aren't read
automatically, only referenced, unless you ask for them specifically.