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
+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