Fixed setups

This commit is contained in:
Henner M. Kruse
2026-08-04 11:15:24 +02:00
parent e485d33f00
commit 6c6fc35a1f
9 changed files with 609 additions and 288 deletions
+42 -22
View File
@@ -5,8 +5,10 @@ description: Manages git operations (status, log, diff, show, fetch, remote, bra
# Git Manager
This skill wraps all git operations in a single script,
`scripts/git_cmd.sh`, so that:
This skill wraps all git operations in a single script, permanently
installed at `~/.agent-skills/git-manager/bin/git_cmd.sh` by this plugin's
setup (not run from wherever this skill's own files happen to live — see
"Setup and permissions" below for why), so that:
- There is never a need for `cd <path> && git ...` to operate on a
non-default repository — pass `--repo <name>` instead.
@@ -26,28 +28,44 @@ back to prompts (or, worse, slipping through on a stale broad rule).
Always use:
```bash
<skill-dir>/scripts/git_cmd.sh <subcommand> [--repo <name>] [-- <git-args...>]
~/.agent-skills/git-manager/bin/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.
- `--repo <name>` targets a specific pre-configured repository. The script
itself (not this skill's own logic) decides what happens if it's
omitted: with exactly one repo registered in
`~/.agent-skills/git-manager/config.json`, that one is used
automatically; with more than one, the script errors and lists the
registered names so you can retry with `--repo`; with none registered,
it errors saying to register one first. The current working directory
is never consulted for this — whether it happens to be a git repository
itself is not a signal this skill acts on. If the task names a repo
explicitly, pass `--repo <name>` for that one regardless of how many are
registered.
- Everything after `--` is passed through to `git` literally (e.g.
`-- -m "commit message"`, `-- --oneline -10`, `-- origin main`).
- Don't pre-check whether `~/.agent-skills/git-manager/bin/git_cmd.sh`
exists with a separate command (e.g. `test -x ... && ...`) before
calling it — that's exactly the kind of chained command this skill exists
to avoid, and it'll trigger the anti-chaining hook if installed. Just
invoke it directly. If it fails with "No such file or directory" (or
similar), that means setup hasn't been run for this tool yet — tell the
user to run it first (for Claude Code: `/git-manager:setup`) rather than
trying to invoke the script from wherever this skill's own files happen
to be mounted (a plugin cache, a symlink target, etc.) — that location
isn't guaranteed stable and isn't what gets whitelisted.
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
~/.agent-skills/git-manager/bin/git_cmd.sh status
~/.agent-skills/git-manager/bin/git_cmd.sh log -- --oneline -10
~/.agent-skills/git-manager/bin/git_cmd.sh add -- -A
~/.agent-skills/git-manager/bin/git_cmd.sh commit -- -m "Update DNS notes"
~/.agent-skills/git-manager/bin/git_cmd.sh push --repo homelab-notes -- origin main
```
Never call `git` directly, and never use `cd` to switch into a different
@@ -62,11 +80,13 @@ purpose as a guardrail, not an oversight.
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
```
ask the user for its absolute path and add it. Use the Read/Write file
tools for this, not `cat`/Bash — the whitelisted permission for this file
is `Read(~/.agent-skills/git-manager/config.json)`, which covers the Read
file tool, not a Bash `cat` invocation of the same path (those are
separate permission checks in Claude Code, even though they access the
same file). Read the file (it may not exist yet, that's fine — treat that
as no repos registered):
```json
{
@@ -77,9 +97,9 @@ cat ~/.agent-skills/git-manager/config.json 2>/dev/null
}
```
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.
Merge new entries in rather than overwriting existing ones. At least one
repo must be registered for this skill to do anything there is no
current-working-directory fallback.
## Setup and permissions
@@ -89,7 +109,7 @@ 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
`Bash(~/.agent-skills/git-manager/bin/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
+36 -24
View File
@@ -17,41 +17,53 @@ is_allowed_subcommand() {
return 1
}
# Resolve a repo's absolute path.
# Resolve a repo's absolute path. Only ever looks at configured repos —
# never falls back to the current working directory, regardless of
# whether it happens to be a git repo itself. This is deliberate: the
# current directory being a git repo is not a signal this script acts on.
# - 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).
# - If no name is given, look at the config: exactly one registered repo
# is used automatically; zero or more than one is an error requiring an
# explicit --repo.
# Never accepts a raw path from the caller directly — only ever a name
# resolved through the config file, so the whitelisted script can't be
# pointed at an arbitrary directory outside what's configured.
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'
if [ ! -f "$CONFIG_FILE" ]; then
echo "Error: no configuration found at $CONFIG_FILE. Register at least one repo first (run setup, or ask to register one)." >&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")
if repo_name:
match = [r for r in repos if r.get("name") == repo_name]
if not match:
names = ", ".join(r.get("name", "?") for r in repos) or "(none)"
sys.stderr.write(f"Error: no repo named '{repo_name}' in config. Registered: {names}\n")
sys.exit(1)
print(match[0]["path"])
elif len(repos) == 1:
print(repos[0]["path"])
elif len(repos) == 0:
sys.stderr.write("Error: no repos registered in config. Register at least one first.\n")
sys.exit(1)
else:
names = ", ".join(r.get("name", "?") for r in repos)
sys.stderr.write(f"Error: multiple repos configured ({names}); specify one with --repo <name>.\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