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
+7
View File
@@ -12,6 +12,13 @@
"version": "1.0.0",
"description": "Obsidian vault as a searchable knowledge base for lookup and note-taking.",
"keywords": ["obsidian", "knowledge-base", "notes", "vault", "markdown"]
},
{
"name": "git-manager",
"source": "./vendor/claude-code/plugins/git-manager",
"version": "1.0.0",
"description": "Whitelisted git operations across one or more repositories via a single wrapper script, avoiding cd/chaining patterns that break Bash permission whitelisting.",
"keywords": ["git", "version-control", "workflow"]
}
]
}
+88
View File
@@ -0,0 +1,88 @@
# skill-repo
Private collection of tool-neutral Agent Skills, plus optional vendor-specific
setup/integration adapters (currently: Claude Code).
## Why this structure
The `SKILL.md` format (YAML frontmatter + Markdown body + `scripts/` /
`references/` / `assets/`) is an open standard published at
[agentskills.io](https://agentskills.io), adopted by Claude Code, OpenAI
Codex, GitHub Copilot, Cursor, Gemini CLI, and others — the same skill files
work unmodified across these tools. What's *not* portable is host-specific
plumbing: slash commands, permission/allow-list syntax, plugin/marketplace
manifests. This repo keeps those two concerns physically separate.
## Structure
```
skill-repo/
├── skills/ ← tool-neutral, single source of truth
│ └── obsidian-vault-kb/
│ ├── SKILL.md
│ ├── references/
│ └── scripts/ ← config lives at ~/.agent-skills/<skill>/config.json
├── vendor/
│ └── claude-code/
│ ├── .claude-plugin/ ← (unused here; marketplace.json lives at repo root, see below)
│ └── plugins/
│ └── obsidian-vault-kb/
│ ├── .claude-plugin/
│ │ └── plugin.json
│ ├── skills/
│ │ └── obsidian-vault-kb -> ../../../../../skills/obsidian-vault-kb (symlink)
│ ├── commands/
│ │ └── setup.md ← -> /obsidian-vault-kb:setup
│ └── permissions-whitelist.template.json
└── .claude-plugin/
└── marketplace.json ← must live at repo root per Claude Code's convention
```
**`skills/<name>/`** is the only place skill content actually lives —
`SKILL.md`, `scripts/`, `references/`. It assumes nothing about the host
tool: configuration is read from `~/.agent-skills/<skill-name>/config.json`,
a tool-neutral location, not `~/.claude/...`.
**`vendor/<tool>/`** holds everything specific to one host tool: for Claude
Code, that's the plugin manifest, the permissions whitelist (Claude Code's
`Bash(...)` allow-list syntax), and the `/setup` slash command. The plugin's
`skills/<name>` is a **symlink** into the top-level `skills/` directory —
there is exactly one copy of the skill content on disk, never a duplicate
that can drift out of sync.
Adding support for another tool (e.g. a future OpenAI/Codex-specific
adapter) means adding `vendor/<other-tool>/` with that tool's own
conventions, symlinked back to the same `skills/<name>/`, without touching
the skill content itself.
Symlinks assume a POSIX filesystem; this repo doesn't attempt to be
Windows-git-checkout-friendly.
## Adding this marketplace locally (Claude Code)
```
/plugin marketplace add /absolute/path/to/skill-repo
/plugin install obsidian-vault-kb
```
Then run `/obsidian-vault-kb:setup` to configure the vault path/mode and the
permissions whitelist.
## Adding a new skill to this repo
1. Create `skills/<new-skill-name>/` with `SKILL.md`, and `scripts/`
/`references/` as needed. Keep configuration at
`~/.agent-skills/<new-skill-name>/config.json` if the skill needs
persistent config — don't hardcode a Claude-specific path here.
2. For Claude Code support, create
`vendor/claude-code/plugins/<new-skill-name>/` with `.claude-plugin/plugin.json`,
a `skills/<new-skill-name>` symlink back to the top-level `skills/`
directory, and (if the skill needs restricted shell access) a
`commands/setup.md` plus `permissions-whitelist.template.json` following
the `obsidian-vault-kb` pattern — wrapper scripts that resolve their own
scope from the config file rather than accepting raw paths as arguments,
whitelisted only by exact absolute path, never generic tool wildcards
like `Bash(find:*)`.
3. Add an entry for the new plugin to `.claude-plugin/marketplace.json`.
4. Bump `version` in the plugin's `plugin.json` and its `marketplace.json`
entry on updates.
+56 -72
View File
@@ -1,88 +1,72 @@
# skill-repo
Private collection of tool-neutral Agent Skills, plus optional vendor-specific
setup/integration adapters (currently: Claude Code).
A collection of Claude Code plugins (Agent Skills) for working with git
repositories and Obsidian vaults. Each plugin does one job, ships its own
setup command, and only asks for the minimum shell access it actually
needs.
## Why this structure
> Looking for how this repo is structured internally, or how to add a new
> plugin to it? See [ARCHITECTURE.md](ARCHITECTURE.md). This README is
> about what's in the box and how to use it.
The `SKILL.md` format (YAML frontmatter + Markdown body + `scripts/` /
`references/` / `assets/`) is an open standard published at
[agentskills.io](https://agentskills.io), adopted by Claude Code, OpenAI
Codex, GitHub Copilot, Cursor, Gemini CLI, and others — the same skill files
work unmodified across these tools. What's *not* portable is host-specific
plumbing: slash commands, permission/allow-list syntax, plugin/marketplace
manifests. This repo keeps those two concerns physically separate.
## What's in here
## Structure
| Plugin | What it does |
|---|---|
| [`git-manager`](vendor/claude-code/plugins/git-manager/README.md) | Runs git commands (status, log, commit, push, pull, ...) across one or more repositories through a single whitelisted script — no more permission prompts breaking on `cd && git ...` chains. |
| [`obsidian-vault-kb`](vendor/claude-code/plugins/obsidian-vault-kb/README.md) | Turns an Obsidian vault into a searchable knowledge base Claude can consult and (optionally) write back to. |
```
skill-repo/
├── skills/ ← tool-neutral, single source of truth
│ └── obsidian-vault-kb/
│ ├── SKILL.md
│ ├── references/
│ └── scripts/ ← config lives at ~/.agent-skills/<skill>/config.json
├── vendor/
│ └── claude-code/
│ ├── .claude-plugin/ ← (unused here; marketplace.json lives at repo root, see below)
│ └── plugins/
│ └── obsidian-vault-kb/
│ ├── .claude-plugin/
│ │ └── plugin.json
│ ├── skills/
│ │ └── obsidian-vault-kb -> ../../../../../skills/obsidian-vault-kb (symlink)
│ ├── commands/
│ │ └── setup.md ← -> /obsidian-vault-kb:setup
│ └── permissions-whitelist.template.json
└── .claude-plugin/
└── marketplace.json ← must live at repo root per Claude Code's convention
```
Click through to each plugin's own README for details, examples, and setup
options.
**`skills/<name>/`** is the only place skill content actually lives —
`SKILL.md`, `scripts/`, `references/`. It assumes nothing about the host
tool: configuration is read from `~/.agent-skills/<skill-name>/config.json`,
a tool-neutral location, not `~/.claude/...`.
## Installing
**`vendor/<tool>/`** holds everything specific to one host tool: for Claude
Code, that's the plugin manifest, the permissions whitelist (Claude Code's
`Bash(...)` allow-list syntax), and the `/setup` slash command. The plugin's
`skills/<name>` is a **symlink** into the top-level `skills/` directory —
there is exactly one copy of the skill content on disk, never a duplicate
that can drift out of sync.
Adding support for another tool (e.g. a future OpenAI/Codex-specific
adapter) means adding `vendor/<other-tool>/` with that tool's own
conventions, symlinked back to the same `skills/<name>/`, without touching
the skill content itself.
Symlinks assume a POSIX filesystem; this repo doesn't attempt to be
Windows-git-checkout-friendly.
## Adding this marketplace locally (Claude Code)
From within Claude Code:
```
/plugin marketplace add /absolute/path/to/skill-repo
```
(or a Git URL, once this repo is hosted somewhere reachable). Then install
whichever plugin(s) you want:
```
/plugin install git-manager
/plugin install obsidian-vault-kb
```
Then run `/obsidian-vault-kb:setup` to configure the vault path/mode and the
permissions whitelist.
Each plugin has its own `/plugin-name:setup` command — run it once after
installing to configure paths and permissions. See the individual plugin
READMEs for what each setup asks for.
## Adding a new skill to this repo
## A shared design philosophy
1. Create `skills/<new-skill-name>/` with `SKILL.md`, and `scripts/`
/`references/` as needed. Keep configuration at
`~/.agent-skills/<new-skill-name>/config.json` if the skill needs
persistent config — don't hardcode a Claude-specific path here.
2. For Claude Code support, create
`vendor/claude-code/plugins/<new-skill-name>/` with `.claude-plugin/plugin.json`,
a `skills/<new-skill-name>` symlink back to the top-level `skills/`
directory, and (if the skill needs restricted shell access) a
`commands/setup.md` plus `permissions-whitelist.template.json` following
the `obsidian-vault-kb` pattern — wrapper scripts that resolve their own
scope from the config file rather than accepting raw paths as arguments,
whitelisted only by exact absolute path, never generic tool wildcards
like `Bash(find:*)`.
3. Add an entry for the new plugin to `.claude-plugin/marketplace.json`.
4. Bump `version` in the plugin's `plugin.json` and its `marketplace.json`
entry on updates.
Both plugins in this repo follow the same rules, worth knowing upfront:
- **Minimal permissions.** Neither plugin whitelists generic commands like
`Bash(git:*)`, `Bash(find:*)`, or `Bash(cd:*)`. Each whitelists exactly
one wrapper script, pinned to its exact absolute path. The wrapper script
itself enforces what it's allowed to do (allowed git subcommands, vault
boundaries) — the whitelist just says "this specific, self-limiting
script may run without asking."
- **Nothing destructive is silent.** Actions that change files — writing
notes, pushing to a remote — either aren't whitelisted (so you still get
a normal confirmation prompt) or are scoped tightly enough that there's
nothing surprising they can do.
- **Config lives outside Claude Code**, at `~/.agent-skills/<plugin-name>/config.json`.
Both plugins follow the open [Agent Skills](https://agentskills.io)
format for their actual skill content (`SKILL.md` + `scripts/`), so that
part works the same if you ever run them from a different
Agent-Skills-compatible tool — only the Claude Code-specific setup
command and permissions live under `vendor/claude-code/`.
## Updating
After pulling changes to this repo:
```
/plugin marketplace update skill-repo
```
Re-run a plugin's `/plugin-name:setup` if its permissions or config
format changed (check that plugin's README/changelog).
+97
View File
@@ -0,0 +1,97 @@
---
name: git-manager
description: Manages git operations (status, log, diff, show, fetch, remote, branch, checkout, add, commit, push, pull) across one or more git repositories — including non-code repositories such as an Obsidian vault kept under version control — through a single wrapper script instead of raw shell `git`/`cd` commands. Always use this skill instead of running `git` or `cd` directly whenever the task involves checking status, committing, pushing, pulling, or otherwise managing a git repository, especially when multiple repositories are involved in the same session (e.g. a code repo and a separate notes/vault repo).
---
# Git Manager
This skill wraps all git operations in a single script,
`scripts/git_cmd.sh`, so that:
- There is never a need for `cd <path> && git ...` to operate on a
non-default repository — pass `--repo <name>` instead.
- There is never a need to chain multiple git commands with `&&`/`;`/`|` in
one shell invocation — each git operation is its own separate call to the
wrapper.
This matters beyond convenience: on the host tool side (see "Setup and
permissions" below), only this single wrapper script is whitelisted to run
without a confirmation prompt. Chained or `cd`-based commands don't match
that whitelist entry, so avoiding them isn't just tidier — it's what keeps
every git operation actually covered by the whitelist instead of falling
back to prompts (or, worse, slipping through on a stale broad rule).
## How to run git commands
Always use:
```bash
<skill-dir>/scripts/git_cmd.sh <subcommand> [--repo <name>] [-- <git-args...>]
```
- `<subcommand>` must be one of: `status`, `log`, `diff`, `show`, `fetch`,
`remote`, `branch`, `checkout`, `add`, `commit`, `push`, `pull`. Anything
else is rejected by the script itself before git runs.
- `--repo <name>` is optional. Omit it to operate on the git repository
containing the current working directory (this is the normal case for a
single-repo Claude Code project/session). Use it to target a different,
pre-configured repository without changing the working directory — e.g.
a separate Obsidian vault repo alongside a code repo in the same session.
- Everything after `--` is passed through to `git` literally (e.g.
`-- -m "commit message"`, `-- --oneline -10`, `-- origin main`).
Examples:
```bash
<skill-dir>/scripts/git_cmd.sh status
<skill-dir>/scripts/git_cmd.sh log -- --oneline -10
<skill-dir>/scripts/git_cmd.sh add -- -A
<skill-dir>/scripts/git_cmd.sh commit -- -m "Update DNS notes"
<skill-dir>/scripts/git_cmd.sh push --repo homelab-notes -- origin main
```
Never call `git` directly, and never use `cd` to switch into a different
repo before running git — use `--repo` instead. If a task genuinely needs a
subcommand outside the allowed list (e.g. `stash`, `merge`, `rebase`,
`reset`, `tag`), say so explicitly to the user rather than working around
the restriction (e.g. via `git -C` called outside this script, or editing
`.git` internals directly) — that path isn't whitelisted and existing on
purpose as a guardrail, not an oversight.
## Multi-repo setup
If a task refers to a named repo (e.g. "push the homelab-notes vault") and
`~/.agent-skills/git-manager/config.json` doesn't have an entry for it yet,
ask the user for its absolute path and add it:
```bash
cat ~/.agent-skills/git-manager/config.json 2>/dev/null
```
```json
{
"repos": [
{"name": "homelab-notes", "path": "/absolute/path/to/vault"},
{"name": "dwh-pipeline", "path": "/absolute/path/to/repo"}
]
}
```
Merge new entries in rather than overwriting existing ones. This file is
optional — omitting `--repo` and relying on the current working directory
works without any configuration at all.
## Setup and permissions
Configuration lives at the tool-neutral path
`~/.agent-skills/git-manager/config.json`, not a Claude-specific location.
Host-specific setup (permissions whitelist, hooks) lives under `vendor/<tool>/`
in this repo, not in this skill itself. For Claude Code, see
`vendor/claude-code/plugins/git-manager/commands/setup.md`
(`/git-manager:setup`), which whitelists exactly one command —
`Bash(<skill-dir>/scripts/git_cmd.sh:*)` — and optionally installs a
PreToolUse hook that forces a normal confirmation prompt (not a silent
block, not a silent allow) for any Bash call containing shell chaining
operators (`&&`, `;`, `|`, backticks, `$(...)`), as a safety net against
Claude Code's own permission-matching not always splitting compound
commands correctly.
+75
View File
@@ -0,0 +1,75 @@
#!/usr/bin/env bash
# Shared helpers for the git-manager skill. Sourced by git_cmd.sh, not
# meant to be executed directly.
CONFIG_FILE="$HOME/.agent-skills/git-manager/config.json"
# Subcommands this skill is allowed to run. Anything not in this list is
# rejected outright by git_cmd.sh, regardless of what the caller asks for.
ALLOWED_SUBCOMMANDS=(status log diff show fetch remote branch checkout add commit push pull)
is_allowed_subcommand() {
local sub="$1"
local allowed
for allowed in "${ALLOWED_SUBCOMMANDS[@]}"; do
[ "$sub" = "$allowed" ] && return 0
done
return 1
}
# Resolve a repo's absolute path.
# - If a name is given, look it up in the config file's "repos" array.
# - If no name is given, default to $PWD, but only if it's actually inside
# a git work tree.
# Never accepts a raw path from the caller directly — named repos always go
# through the config file, so the whitelisted script can't be pointed at an
# arbitrary directory outside what's configured (or the current project
# directory Claude Code itself already scoped the session to).
resolve_repo_path() {
local repo_name="${1:-}"
if [ -n "$repo_name" ]; then
if [ ! -f "$CONFIG_FILE" ]; then
echo "Error: no configuration found at $CONFIG_FILE. Run setup first, or omit the repo name to use the current directory." >&2
exit 1
fi
if ! command -v python3 >/dev/null 2>&1; then
echo "Error: python3 is required to parse the config file." >&2
exit 1
fi
python3 - "$CONFIG_FILE" "$repo_name" << 'PYEOF'
import json, sys
config_file, repo_name = sys.argv[1], sys.argv[2]
with open(config_file) as f:
cfg = json.load(f)
repos = cfg.get("repos", [])
match = [r for r in repos if r.get("name") == repo_name]
if not match:
sys.stderr.write(f"Error: no repo named '{repo_name}' in config.\n")
sys.exit(1)
print(match[0]["path"])
PYEOF
else
echo "$PWD"
fi
}
# Validate that a resolved path is a real, existing directory that is
# actually inside a git work tree. Prints the canonical repo root.
canonicalize_and_check_repo() {
local path="$1"
local real
real=$(realpath -e "$path" 2>/dev/null) || {
echo "Error: path '$path' does not exist." >&2
exit 1
}
if [ ! -d "$real" ]; then
echo "Error: path '$real' is not a directory." >&2
exit 1
fi
if ! git -C "$real" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
echo "Error: '$real' is not inside a git work tree." >&2
exit 1
fi
git -C "$real" rev-parse --show-toplevel
}
+74
View File
@@ -0,0 +1,74 @@
#!/usr/bin/env bash
# Single entry point for all git operations in this skill. Designed so
# there is never a reason to use `cd` or shell chaining (&&, ;, |) to run
# git commands across different repos in one session.
#
# Usage: git_cmd.sh <subcommand> [--repo <name>] [-- <git-args...>]
#
# Examples:
# git_cmd.sh status
# git_cmd.sh log --repo homelab-notes -- --oneline -10
# git_cmd.sh commit --repo homelab-notes -- -m "Update DNS notes"
# git_cmd.sh push --repo homelab-notes -- origin main
#
# - <subcommand> must be one of the allowed subcommands in _lib.sh; anything
# else is rejected before git is ever invoked.
# - --repo <name> is optional. If omitted, operates on the current working
# directory (which must already be inside a git work tree) — this is what
# replaces `cd <path> && git ...`, since Claude Code already sets the
# working directory per project/session.
# - Everything after `--` is passed through to git as-is via argv (not
# re-interpreted by a shell), so quoting/spaces in e.g. commit messages
# are safe.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=_lib.sh
source "$SCRIPT_DIR/_lib.sh"
SUBCOMMAND="${1:-}"
if [ -z "$SUBCOMMAND" ]; then
echo "Usage: git_cmd.sh <subcommand> [--repo <name>] [-- <git-args...>]" >&2
echo "Allowed subcommands: ${ALLOWED_SUBCOMMANDS[*]}" >&2
exit 1
fi
shift
if ! is_allowed_subcommand "$SUBCOMMAND"; then
echo "Error: subcommand '$SUBCOMMAND' is not allowed." >&2
echo "Allowed subcommands: ${ALLOWED_SUBCOMMANDS[*]}" >&2
exit 1
fi
REPO_NAME=""
GIT_ARGS=()
while [ $# -gt 0 ]; do
case "$1" in
--repo)
REPO_NAME="${2:-}"
shift 2
;;
--)
shift
GIT_ARGS=("$@")
break
;;
*)
echo "Error: unexpected argument '$1'. Put git flags/args after '--'." >&2
exit 1
;;
esac
done
REPO_RAW="$(resolve_repo_path "$REPO_NAME")"
REPO="$(canonicalize_and_check_repo "$REPO_RAW")"
echo "== Repo: $REPO =="
echo "== Running: git $SUBCOMMAND ${GIT_ARGS[*]:-} =="
echo
if [ "${#GIT_ARGS[@]}" -eq 0 ]; then
git -C "$REPO" "$SUBCOMMAND"
else
git -C "$REPO" "$SUBCOMMAND" "${GIT_ARGS[@]}"
fi
@@ -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.