From e485d33f0091f1606e543ea430269d1ca7f18d2b Mon Sep 17 00:00:00 2001 From: "Henner M. Kruse" Date: Tue, 4 Aug 2026 10:45:25 +0200 Subject: [PATCH] Added git-manager, updated READMEs --- .claude-plugin/marketplace.json | 7 + ARCHITECTURE.md | 88 +++++++++++ README.md | 128 +++++++--------- skills/git-manager/SKILL.md | 97 +++++++++++++ skills/git-manager/scripts/_lib.sh | 75 ++++++++++ skills/git-manager/scripts/git_cmd.sh | 74 ++++++++++ .../git-manager/.claude-plugin/plugin.json | 9 ++ .../claude-code/plugins/git-manager/README.md | 122 ++++++++++++++++ .../plugins/git-manager/commands/setup.md | 137 ++++++++++++++++++ .../hooks/force-ask-on-chaining.sh | 47 ++++++ .../permissions-whitelist.template.json | 14 ++ .../plugins/git-manager/skills/git-manager | 1 + .../plugins/obsidian-vault-kb/README.md | 103 +++++++++++++ 13 files changed, 830 insertions(+), 72 deletions(-) create mode 100644 ARCHITECTURE.md create mode 100644 skills/git-manager/SKILL.md create mode 100755 skills/git-manager/scripts/_lib.sh create mode 100755 skills/git-manager/scripts/git_cmd.sh create mode 100644 vendor/claude-code/plugins/git-manager/.claude-plugin/plugin.json create mode 100644 vendor/claude-code/plugins/git-manager/README.md create mode 100644 vendor/claude-code/plugins/git-manager/commands/setup.md create mode 100755 vendor/claude-code/plugins/git-manager/hooks/force-ask-on-chaining.sh create mode 100644 vendor/claude-code/plugins/git-manager/permissions-whitelist.template.json create mode 120000 vendor/claude-code/plugins/git-manager/skills/git-manager create mode 100644 vendor/claude-code/plugins/obsidian-vault-kb/README.md diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index d412a6e..7026471 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -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"] } ] } diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..bd67450 --- /dev/null +++ b/ARCHITECTURE.md @@ -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//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//`** 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//config.json`, +a tool-neutral location, not `~/.claude/...`. + +**`vendor//`** 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/` 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//` with that tool's own +conventions, symlinked back to the same `skills//`, 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//` with `SKILL.md`, and `scripts/` + /`references/` as needed. Keep configuration at + `~/.agent-skills//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//` with `.claude-plugin/plugin.json`, + a `skills/` 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. diff --git a/README.md b/README.md index bd67450..3067c9a 100644 --- a/README.md +++ b/README.md @@ -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//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//`** 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//config.json`, -a tool-neutral location, not `~/.claude/...`. +## Installing -**`vendor//`** 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/` 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//` with that tool's own -conventions, symlinked back to the same `skills//`, 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//` with `SKILL.md`, and `scripts/` - /`references/` as needed. Keep configuration at - `~/.agent-skills//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//` with `.claude-plugin/plugin.json`, - a `skills/` 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//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). diff --git a/skills/git-manager/SKILL.md b/skills/git-manager/SKILL.md new file mode 100644 index 0000000..ec63a5f --- /dev/null +++ b/skills/git-manager/SKILL.md @@ -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 && git ...` to operate on a + non-default repository — pass `--repo ` 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 +/scripts/git_cmd.sh [--repo ] [-- ] +``` + +- `` 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 ` 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 +/scripts/git_cmd.sh status +/scripts/git_cmd.sh log -- --oneline -10 +/scripts/git_cmd.sh add -- -A +/scripts/git_cmd.sh commit -- -m "Update DNS notes" +/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//` +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(/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. diff --git a/skills/git-manager/scripts/_lib.sh b/skills/git-manager/scripts/_lib.sh new file mode 100755 index 0000000..2f73452 --- /dev/null +++ b/skills/git-manager/scripts/_lib.sh @@ -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 +} diff --git a/skills/git-manager/scripts/git_cmd.sh b/skills/git-manager/scripts/git_cmd.sh new file mode 100755 index 0000000..4130959 --- /dev/null +++ b/skills/git-manager/scripts/git_cmd.sh @@ -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 [--repo ] [-- ] +# +# 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 +# +# - must be one of the allowed subcommands in _lib.sh; anything +# else is rejected before git is ever invoked. +# - --repo is optional. If omitted, operates on the current working +# directory (which must already be inside a git work tree) — this is what +# replaces `cd && 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 [--repo ] [-- ]" >&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 diff --git a/vendor/claude-code/plugins/git-manager/.claude-plugin/plugin.json b/vendor/claude-code/plugins/git-manager/.claude-plugin/plugin.json new file mode 100644 index 0000000..8f81789 --- /dev/null +++ b/vendor/claude-code/plugins/git-manager/.claude-plugin/plugin.json @@ -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"] +} diff --git a/vendor/claude-code/plugins/git-manager/README.md b/vendor/claude-code/plugins/git-manager/README.md new file mode 100644 index 0000000..792ba17 --- /dev/null +++ b/vendor/claude-code/plugins/git-manager/README.md @@ -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 [--repo ] [-- ] +``` + +- **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 ` 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(:*)` — 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. diff --git a/vendor/claude-code/plugins/git-manager/commands/setup.md b/vendor/claude-code/plugins/git-manager/commands/setup.md new file mode 100644 index 0000000..0ff9f17 --- /dev/null +++ b/vendor/claude-code/plugins/git-manager/commands/setup.md @@ -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 /skills/git-manager +``` + +This resolved path is `` 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": "", "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(/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, ``. +2. Ensure it's executable: `chmod +x `. +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": "" + } + ] + } + ] + } +} +``` + +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 /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 `/scripts/git_cmd.sh status` (against the +current directory, or a registered repo) as a quick sanity check. diff --git a/vendor/claude-code/plugins/git-manager/hooks/force-ask-on-chaining.sh b/vendor/claude-code/plugins/git-manager/hooks/force-ask-on-chaining.sh new file mode 100755 index 0000000..6a776c4 --- /dev/null +++ b/vendor/claude-code/plugins/git-manager/hooks/force-ask-on-chaining.sh @@ -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 diff --git a/vendor/claude-code/plugins/git-manager/permissions-whitelist.template.json b/vendor/claude-code/plugins/git-manager/permissions-whitelist.template.json new file mode 100644 index 0000000..367ab6f --- /dev/null +++ b/vendor/claude-code/plugins/git-manager/permissions-whitelist.template.json @@ -0,0 +1,14 @@ +{ + "_comment": "Template for the git-manager skill's Claude Code permissions. 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(/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." + ] +} diff --git a/vendor/claude-code/plugins/git-manager/skills/git-manager b/vendor/claude-code/plugins/git-manager/skills/git-manager new file mode 120000 index 0000000..3494c70 --- /dev/null +++ b/vendor/claude-code/plugins/git-manager/skills/git-manager @@ -0,0 +1 @@ +../../../../../skills/git-manager \ No newline at end of file diff --git a/vendor/claude-code/plugins/obsidian-vault-kb/README.md b/vendor/claude-code/plugins/obsidian-vault-kb/README.md new file mode 100644 index 0000000..bbeb676 --- /dev/null +++ b/vendor/claude-code/plugins/obsidian-vault-kb/README.md @@ -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 ""` — full-text search, optionally scoped to a + subfolder +- `vault_backlinks.sh ""` — 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.