Fixed setups
This commit is contained in:
+77
-118
@@ -3,135 +3,94 @@ description: Set up the git-manager skill (permissions whitelist, optional anti-
|
||||
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.
|
||||
Set up the `git-manager` skill. As part of the `git-manager` plugin, this
|
||||
command is automatically namespaced by Claude Code and invoked as
|
||||
`/git-manager:setup`.
|
||||
|
||||
## 1. Determine the skill's install directory
|
||||
Almost everything about this setup — reading/writing the skill's config,
|
||||
merging permissions and the hook into settings.json, setting executable
|
||||
bits — happens inside one bundled script, `scripts/setup.sh`, which you run
|
||||
as a **single** Bash call. That script also copies the wrapper script(s) it
|
||||
needs to a stable, self-controlled location
|
||||
(`~/.agent-skills/git-manager/bin/`) before writing any permissions, so the
|
||||
permission rule never has to change again across future plugin updates —
|
||||
only this first run (and, optionally, a re-run after a plugin update) needs
|
||||
to locate the plugin itself.
|
||||
|
||||
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:
|
||||
## 1. Ask the user — three selectable questions first, repo path last
|
||||
|
||||
**1a. First turn: three questions via the selection UI, fixed options for
|
||||
all three:**
|
||||
- Whether to register any repos at all: yes/no.
|
||||
- Settings scope: `project` (`.claude/settings.json` in the current
|
||||
project) or `user` (`~/.claude/settings.json`). Mention: if they work
|
||||
across many separate repos/projects, `user` avoids repeating setup per
|
||||
project.
|
||||
- Anti-chaining hook: yes/no, after explaining it never silently blocks or
|
||||
silently allows anything, it only forces the *normal* confirmation
|
||||
prompt when a Bash command contains shell chaining operators (`&&`, `;`,
|
||||
`|`, backticks, `$(...)`), as a safety net given Claude Code's Bash
|
||||
allow-list matching has had bugs around compound commands (see
|
||||
`hooks/force-ask-on-chaining.sh` for details).
|
||||
|
||||
**1b. Only if the answer to "register any repos" was yes: a second turn,
|
||||
plain chat message, no tool call.** Ask exactly: "Which repos should
|
||||
git-manager know about? Provide as `name=path` pairs." End your turn right
|
||||
after asking this, with nothing else queued up, so the user's reply is
|
||||
what actually gets collected before you continue. (Only a tool call
|
||||
reliably pauses for a reply in this environment; plain text followed
|
||||
immediately by a tool call in the same turn does not wait — that's what
|
||||
caused this question to appear skipped in an earlier version of this
|
||||
command.) If the answer to "register any repos" was no, skip this
|
||||
sub-step entirely — don't ask it at all.
|
||||
|
||||
## 2. Run the setup script — one command
|
||||
|
||||
Claude Code installs plugins added from a local marketplace at
|
||||
`~/.claude/plugins/cache/<marketplace-name>/<plugin-name>/<plugin-version>/`.
|
||||
For this repo's marketplace (`skill-repo`) and this plugin's current
|
||||
version (`1.0.0` — check this plugin's own `.claude-plugin/plugin.json` if
|
||||
this has since changed), that's:
|
||||
|
||||
```bash
|
||||
realpath <plugin-dir>/skills/git-manager
|
||||
~/.claude/plugins/cache/skill-repo/git-manager/1.0.0/scripts/setup.sh \
|
||||
--settings-scope <project|user> \
|
||||
[--project-dir <path>] \
|
||||
[--repo <name>=<path> ...] \
|
||||
[--install-hook]
|
||||
```
|
||||
|
||||
This resolved path is `<SKILL_DIR>` for the rest of this setup.
|
||||
Include `--repo name=path` once per repo from step 1, `--install-hook` only
|
||||
if the user opted in, and `--project-dir` only if `--settings-scope
|
||||
project` and the project isn't the current working directory. Try this
|
||||
path directly first — don't `find`/`ls` preemptively.
|
||||
|
||||
## 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`:
|
||||
If that exact path doesn't exist (installed version differs from `1.0.0`,
|
||||
differently named marketplace, or a future Claude Code cache layout
|
||||
change), fall back to exactly one bounded lookup instead of guessing
|
||||
further:
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.agent-skills/git-manager
|
||||
cat > ~/.agent-skills/git-manager/config.json << 'EOF'
|
||||
{
|
||||
"repos": [
|
||||
{"name": "<short-name>", "path": "<absolute-repo-path>"}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
find ~/.claude/plugins/cache/skill-repo/git-manager -maxdepth 2 -type d 2>/dev/null
|
||||
```
|
||||
|
||||
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.
|
||||
and construct the same `scripts/setup.sh` call using whatever version
|
||||
directory that reveals. This is expected to prompt for approval the first
|
||||
time — there's no way to pre-whitelist it before the setup that establishes
|
||||
the whitelist has run.
|
||||
|
||||
## 3. Generate and merge the minimal permissions whitelist
|
||||
## 3. Relay the result
|
||||
|
||||
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:
|
||||
The script prints a JSON summary (stable script location, settings file
|
||||
written, config file, whether the hook was installed, repos registered,
|
||||
the exact permission rules added, and a note about session reload).
|
||||
Present that summary to the user in plain language — don't re-read any of
|
||||
those files yourself to "double check"; the script's own output already
|
||||
reflects exactly what's on disk.
|
||||
|
||||
- `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.
|
||||
Do **not** follow this with a test invocation of `git_cmd.sh` — Claude Code
|
||||
loads permissions at session start and doesn't always pick up changes made
|
||||
by its own file edits within the same session, so an immediate test can
|
||||
prompt (or not) in a way that doesn't reliably indicate success either way.
|
||||
If a real command later still prompts, the fix is a fresh session, not
|
||||
re-running this setup — the configuration is already correctly saved.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"_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.",
|
||||
"_comment": "Reference only — setup.sh generates and writes these exact rules itself, so this file is no longer read during setup. Kept here for auditing/documentation: this is what /git-manager:setup will add to your permissions.allow, with <SKILL_DIR> substituted for the real, symlink-resolved skills/git-manager path. Exactly one Bash rule — 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 named entries in ~/.agent-skills/git-manager/config.json, never from the current working directory or an arbitrary caller-supplied path.",
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(<SKILL_DIR>/scripts/git_cmd.sh:*)",
|
||||
|
||||
+186
@@ -0,0 +1,186 @@
|
||||
#!/usr/bin/env bash
|
||||
# One-shot setup for the git-manager plugin.
|
||||
#
|
||||
# Order matters here, deliberately: the permission rule is established
|
||||
# FIRST, against a stable path we control ourselves
|
||||
# (~/.agent-skills/git-manager/bin/), not against Claude Code's internal
|
||||
# plugin cache path (~/.claude/plugins/cache/<marketplace>/<plugin>/<version>/),
|
||||
# which changes on every plugin update and is why earlier versions of this
|
||||
# setup needed to "locate itself" at all. Configuration (repos, hook) comes
|
||||
# after, and re-running this script after a plugin update re-syncs the
|
||||
# stable copy without ever touching the permission rule again.
|
||||
#
|
||||
# Usage:
|
||||
# setup.sh --settings-scope (project|user) [--project-dir <path>]
|
||||
# [--repo <name>=<path> ...] [--install-hook]
|
||||
#
|
||||
# Prints a JSON summary of what changed to stdout at the end.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PLUGIN_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
SKILL_DIR="$(realpath "$PLUGIN_DIR/skills/git-manager")"
|
||||
SOURCE_HOOK_PATH="$PLUGIN_DIR/hooks/force-ask-on-chaining.sh"
|
||||
|
||||
if ! command -v python3 >/dev/null 2>&1; then
|
||||
echo "Error: python3 is required." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- 1. Establish the stable, self-controlled target location and copy
|
||||
# the wrapper script (+ its dependency) and the hook there. This is the
|
||||
# ONLY thing whitelisted permissions will ever point to — never the
|
||||
# Claude Code plugin cache path, which changes on every version bump. ---
|
||||
STABLE_DIR="$HOME/.agent-skills/git-manager/bin"
|
||||
mkdir -p "$STABLE_DIR"
|
||||
cp "$SKILL_DIR/scripts/git_cmd.sh" "$STABLE_DIR/git_cmd.sh"
|
||||
cp "$SKILL_DIR/scripts/_lib.sh" "$STABLE_DIR/_lib.sh"
|
||||
cp "$SOURCE_HOOK_PATH" "$STABLE_DIR/force-ask-on-chaining.sh"
|
||||
chmod +x "$STABLE_DIR/git_cmd.sh" "$STABLE_DIR/force-ask-on-chaining.sh"
|
||||
|
||||
WRAPPER_PATH="$STABLE_DIR/git_cmd.sh"
|
||||
HOOK_PATH="$STABLE_DIR/force-ask-on-chaining.sh"
|
||||
# Tilde-form paths for permission rules and SKILL.md's example commands —
|
||||
# Claude Code matches Bash permission rules against the literal,
|
||||
# unexpanded command text, so a rule written with the resolved absolute
|
||||
# $HOME path (e.g. /home/hmk/...) does NOT match a command Claude typed as
|
||||
# "~/...", even though both point at the same file. Keep both sides in
|
||||
# tilde form so they actually match.
|
||||
TILDE_WRAPPER_PATH="~/.agent-skills/git-manager/bin/git_cmd.sh"
|
||||
TILDE_HOOK_PATH="~/.agent-skills/git-manager/bin/force-ask-on-chaining.sh"
|
||||
TILDE_CONFIG_FILE="~/.agent-skills/git-manager/config.json"
|
||||
|
||||
# --- 2. Parse arguments ---
|
||||
SETTINGS_SCOPE=""
|
||||
PROJECT_DIR="$PWD"
|
||||
INSTALL_HOOK=0
|
||||
REPOS=()
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--settings-scope) SETTINGS_SCOPE="$2"; shift 2 ;;
|
||||
--project-dir) PROJECT_DIR="$2"; shift 2 ;;
|
||||
--repo) REPOS+=("$2"); shift 2 ;;
|
||||
--install-hook) INSTALL_HOOK=1; shift ;;
|
||||
*) echo "Error: unknown argument '$1'" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "$SETTINGS_SCOPE" != "project" ] && [ "$SETTINGS_SCOPE" != "user" ]; then
|
||||
echo "Error: --settings-scope must be 'project' or 'user'" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$SETTINGS_SCOPE" = "project" ]; then
|
||||
SETTINGS_FILE="$PROJECT_DIR/.claude/settings.json"
|
||||
else
|
||||
SETTINGS_FILE="$HOME/.claude/settings.json"
|
||||
fi
|
||||
|
||||
CONFIG_DIR="$HOME/.agent-skills/git-manager"
|
||||
CONFIG_FILE="$CONFIG_DIR/config.json"
|
||||
mkdir -p "$CONFIG_DIR"
|
||||
|
||||
# --- 3. Write permissions FIRST, against the stable path from step 1 —
|
||||
# before touching any repo configuration. This is what makes "permissions
|
||||
# first, then configuration" actually true, per the point of this
|
||||
# redesign: the target path was already fixed and known before we even
|
||||
# parsed arguments. ---
|
||||
mkdir -p "$(dirname "$SETTINGS_FILE")"
|
||||
|
||||
python3 - "$SETTINGS_FILE" "$TILDE_WRAPPER_PATH" "$TILDE_CONFIG_FILE" "$HOOK_PATH" "$INSTALL_HOOK" << 'PYEOF'
|
||||
import json, os, sys
|
||||
|
||||
settings_file, tilde_wrapper_path, tilde_config_file, hook_path, install_hook = sys.argv[1:6]
|
||||
install_hook = install_hook == "1"
|
||||
|
||||
settings = {}
|
||||
if os.path.exists(settings_file):
|
||||
with open(settings_file) as f:
|
||||
settings = json.load(f)
|
||||
|
||||
perms = settings.setdefault("permissions", {})
|
||||
allow = perms.setdefault("allow", [])
|
||||
|
||||
# Tilde form here on purpose: Claude Code matches permission rules against
|
||||
# the literal, unexpanded command text a model produces, which uses "~"
|
||||
# (per SKILL.md), not the resolved absolute $HOME path.
|
||||
new_rules = [
|
||||
f"Bash({tilde_wrapper_path}:*)",
|
||||
f"Read({tilde_config_file})",
|
||||
]
|
||||
for rule in new_rules:
|
||||
if rule not in allow:
|
||||
allow.append(rule)
|
||||
|
||||
if install_hook:
|
||||
hooks = settings.setdefault("hooks", {})
|
||||
pre_tool_use = hooks.setdefault("PreToolUse", [])
|
||||
already_present = any(
|
||||
entry.get("matcher") == "Bash"
|
||||
and any(h.get("command") == hook_path for h in entry.get("hooks", []))
|
||||
for entry in pre_tool_use
|
||||
)
|
||||
if not already_present:
|
||||
pre_tool_use.append({
|
||||
"matcher": "Bash",
|
||||
# Real absolute path here (not tilde) — this is executed
|
||||
# directly by Claude Code as a subprocess, not matched as text,
|
||||
# so it needs to be an actually-invokable path.
|
||||
"hooks": [{"type": "command", "command": hook_path}],
|
||||
})
|
||||
|
||||
with open(settings_file, "w") as f:
|
||||
json.dump(settings, f, indent=2)
|
||||
f.write("\n")
|
||||
PYEOF
|
||||
|
||||
# --- 4. THEN configuration: write/merge repos, if any were given ---
|
||||
REPOS_JSON="[]"
|
||||
if [ "${#REPOS[@]}" -gt 0 ]; then
|
||||
REPOS_JSON=$(python3 -c '
|
||||
import json, sys
|
||||
pairs = sys.argv[1:]
|
||||
repos = []
|
||||
for p in pairs:
|
||||
name, path = p.split("=", 1)
|
||||
repos.append({"name": name, "path": path})
|
||||
print(json.dumps(repos))
|
||||
' "${REPOS[@]}")
|
||||
fi
|
||||
|
||||
python3 - "$CONFIG_FILE" "$REPOS_JSON" << 'PYEOF'
|
||||
import json, os, sys
|
||||
config_file, new_repos_json = sys.argv[1], sys.argv[2]
|
||||
new_repos = json.loads(new_repos_json)
|
||||
|
||||
existing = {"repos": []}
|
||||
if os.path.exists(config_file):
|
||||
with open(config_file) as f:
|
||||
existing = json.load(f)
|
||||
|
||||
existing_by_name = {r["name"]: r for r in existing.get("repos", [])}
|
||||
for r in new_repos:
|
||||
existing_by_name[r["name"]] = r # upsert: new/updated path always wins
|
||||
existing["repos"] = list(existing_by_name.values())
|
||||
|
||||
with open(config_file, "w") as f:
|
||||
json.dump(existing, f, indent=2)
|
||||
f.write("\n")
|
||||
PYEOF
|
||||
|
||||
# --- 5. Summary ---
|
||||
python3 - "$STABLE_DIR" "$SETTINGS_FILE" "$CONFIG_FILE" "$INSTALL_HOOK" "$REPOS_JSON" "$TILDE_WRAPPER_PATH" "$TILDE_CONFIG_FILE" << 'PYEOF'
|
||||
import json, sys
|
||||
stable_dir, settings_file, config_file, install_hook, repos_json, tilde_wrapper_path, tilde_config_file = sys.argv[1:8]
|
||||
print(json.dumps({
|
||||
"stable_script_location": stable_dir,
|
||||
"settings_file": settings_file,
|
||||
"config_file": config_file,
|
||||
"hook_installed": install_hook == "1",
|
||||
"repos_registered": json.loads(repos_json),
|
||||
"permission_rules_added": [f"Bash({tilde_wrapper_path}:*)", f"Read({tilde_config_file})"],
|
||||
"note": "The permission rule points at a stable location this script controls (~/.agent-skills/git-manager/bin/), not at Claude Code's internal plugin cache — so it survives future plugin updates without changing. It's written in tilde form (~/...) to match the literal, unexpanded command text Claude Code matches against, not the resolved absolute $HOME path. Permissions are written to disk now, but Claude Code loads permissions at session start and does not always pick up changes made by its own file edits within the same session — if a git_cmd.sh call still prompts after this, start a fresh session rather than re-running setup."
|
||||
}, indent=2))
|
||||
PYEOF
|
||||
+68
-107
@@ -3,124 +3,85 @@ description: Set up the obsidian-vault-kb skill (vault path, mode, minimal permi
|
||||
argument-hint: [vault-path] [mode]
|
||||
---
|
||||
|
||||
Set up the `obsidian-vault-kb` skill end to end. As part of the
|
||||
`obsidian-vault-kb` plugin, this command is automatically namespaced by
|
||||
Claude Code and invoked as `/obsidian-vault-kb:setup` — no manual filename
|
||||
prefixing needed. Follow these steps in order.
|
||||
Set up the `obsidian-vault-kb` skill. As part of the `obsidian-vault-kb`
|
||||
plugin, this command is automatically namespaced by Claude Code and invoked
|
||||
as `/obsidian-vault-kb:setup`.
|
||||
|
||||
## 1. Determine the skill's install directory
|
||||
Almost everything about this setup — validating vault paths, writing the
|
||||
skill's config, merging permissions into settings.json, setting executable
|
||||
bits — happens inside one bundled script, `scripts/setup.sh`, which you run
|
||||
as a **single** Bash call. That script also copies the wrapper scripts it
|
||||
needs to a stable, self-controlled location
|
||||
(`~/.agent-skills/obsidian-vault-kb/bin/`) before writing any permissions,
|
||||
so the permission rules never have to change again across future plugin
|
||||
updates — only this first run (and, optionally, a re-run after a plugin
|
||||
update) needs to locate the plugin itself.
|
||||
|
||||
This plugin's `skills/obsidian-vault-kb/` is a symlink into the repo's
|
||||
tool-neutral `skills/obsidian-vault-kb/` directory (the single source of
|
||||
truth for `SKILL.md`, `scripts/`, and `references/`, shared across all
|
||||
vendor adapters in this repo). Resolve it to its real, absolute,
|
||||
symlink-free path:
|
||||
## 1. Ask the user — selectable questions first, vault path last
|
||||
|
||||
**1a. First turn: two questions via the selection UI, fixed options for
|
||||
both:**
|
||||
- Mode: `read-only`, `append`, or `maintain` (as defined in SKILL.md —
|
||||
explain each briefly if unsure).
|
||||
- Settings scope: `project` (`.claude/settings.json` in the current
|
||||
project) or `user` (`~/.claude/settings.json`).
|
||||
|
||||
**1b. Second turn, plain chat message, no tool call.** Ask exactly:
|
||||
"What's the path to your vault? Give it a short name too if you're
|
||||
registering more than one." End your turn right after asking this, with
|
||||
nothing else queued up, so the user's reply is what actually gets
|
||||
collected before you continue. (Only a tool call reliably pauses for a
|
||||
reply in this environment; plain text followed immediately by a tool call
|
||||
in the same turn does not wait — that's what caused this question to
|
||||
appear skipped in an earlier version of this command.) At least one vault
|
||||
is required — `setup.sh` errors out without one — so this question is
|
||||
always asked, unlike the optional repos question in the git-manager
|
||||
plugin's setup.
|
||||
|
||||
## 2. Run the setup script — one command
|
||||
|
||||
Claude Code installs plugins added from a local marketplace at
|
||||
`~/.claude/plugins/cache/<marketplace-name>/<plugin-name>/<plugin-version>/`.
|
||||
For this repo's marketplace (`skill-repo`) and this plugin's current
|
||||
version (`1.0.0` — check this plugin's own `.claude-plugin/plugin.json` if
|
||||
this has since changed), that's:
|
||||
|
||||
```bash
|
||||
realpath <plugin-dir>/skills/obsidian-vault-kb
|
||||
~/.claude/plugins/cache/skill-repo/obsidian-vault-kb/1.0.0/scripts/setup.sh \
|
||||
--settings-scope <project|user> \
|
||||
[--project-dir <path>] \
|
||||
--mode <read-only|append|maintain> \
|
||||
--vault <name>=<path> [--vault <name>=<path> ...]
|
||||
```
|
||||
|
||||
This resolved path is `<SKILL_DIR>` for the rest of this setup — you'll
|
||||
need it exactly for step 4. Use the real path (not the symlink path) so the
|
||||
permissions whitelisted in step 4 keep working even if this plugin
|
||||
directory is relocated relative to the shared `skills/` directory.
|
||||
The script itself validates each vault path exists and is a directory, and
|
||||
reports an error if not — no separate validation call needed beforehand.
|
||||
Try this path directly first — don't `find`/`ls` preemptively.
|
||||
|
||||
## 2. Gather vault path and mode
|
||||
|
||||
If `$ARGUMENTS` contains a vault path and/or mode, use those directly.
|
||||
Otherwise ask the user:
|
||||
|
||||
1. Absolute path to the Obsidian vault (if there's more than one vault, ask
|
||||
for a short name per vault too).
|
||||
2. Mode: `read-only`, `append`, or `maintain` (as defined in SKILL.md —
|
||||
explain briefly if the user is unsure which to pick).
|
||||
|
||||
Validate the path exists and is a directory before continuing:
|
||||
If the exact cache path doesn't exist (installed version differs from
|
||||
`1.0.0`, differently named marketplace, or a future cache layout change),
|
||||
fall back to exactly one bounded lookup instead of guessing further:
|
||||
|
||||
```bash
|
||||
test -d "<vault-path>" && echo OK || echo "MISSING"
|
||||
find ~/.claude/plugins/cache/skill-repo/obsidian-vault-kb -maxdepth 2 -type d 2>/dev/null
|
||||
```
|
||||
|
||||
If missing, ask the user to correct it before proceeding.
|
||||
and construct the same `scripts/setup.sh` call using whatever version
|
||||
directory that reveals. This is expected to prompt for approval the first
|
||||
time — there's no way to pre-whitelist it before the setup that establishes
|
||||
the whitelist has run.
|
||||
|
||||
## 3. Write the skill configuration
|
||||
## 3. Relay the result
|
||||
|
||||
The config path is tool-neutral (not Claude-specific), so this same file
|
||||
would be reused if another Agent-Skills-compatible tool ran this skill:
|
||||
The script prints a JSON summary (stable script location, settings file
|
||||
written, config file, mode, vaults registered, the exact permission rules
|
||||
added, and a note about session reload). Present that summary to the user
|
||||
in plain language — don't re-read any of those files yourself to "double
|
||||
check"; the script's own output already reflects exactly what's on disk.
|
||||
|
||||
```bash
|
||||
mkdir -p ~/.agent-skills/obsidian-vault-kb
|
||||
cat > ~/.agent-skills/obsidian-vault-kb/config.json << 'EOF'
|
||||
{
|
||||
"vaults": [
|
||||
{"name": "<short-name>", "path": "<absolute-vault-path>"}
|
||||
],
|
||||
"mode": "<read-only|append|maintain>"
|
||||
}
|
||||
EOF
|
||||
cat ~/.agent-skills/obsidian-vault-kb/config.json
|
||||
```
|
||||
|
||||
Confirm the written content with the user.
|
||||
|
||||
## 4. Generate and merge the minimal permissions whitelist
|
||||
|
||||
Read `permissions-whitelist.template.json` (next to this plugin's
|
||||
`plugin.json`, i.e. `<plugin-dir>/permissions-whitelist.template.json`). It
|
||||
whitelists exactly three commands, each pinned to its full absolute path —
|
||||
no generic `Bash(find:*)`, `Bash(rg:*)`, `Bash(cat:*)`, etc., since those
|
||||
would allow reading anywhere on the filesystem rather than just the vault:
|
||||
|
||||
- `Bash(<SKILL_DIR>/scripts/vault_index.sh:*)`
|
||||
- `Bash(<SKILL_DIR>/scripts/vault_search.sh:*)`
|
||||
- `Bash(<SKILL_DIR>/scripts/vault_backlinks.sh:*)`
|
||||
- `Read(~/.agent-skills/obsidian-vault-kb/config.json)`
|
||||
|
||||
These three scripts resolve the vault path only from
|
||||
`~/.agent-skills/obsidian-vault-kb/config.json` and refuse to operate on any
|
||||
path outside it (see their source), so whitelisting them by exact path does
|
||||
not grant broader filesystem access — not even to other files in the same
|
||||
`scripts/` directory, since the wildcard is per-script-file, not per-folder.
|
||||
|
||||
Steps:
|
||||
|
||||
1. Replace every `<SKILL_DIR>` placeholder in the template with the real,
|
||||
symlink-resolved absolute path from step 1.
|
||||
2. 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.
|
||||
3. If the target file doesn't exist yet, create it containing just the
|
||||
substituted `permissions.allow` array.
|
||||
4. If it exists, read it first and merge: add only entries from the
|
||||
substituted array that aren't already present in the target file's
|
||||
`permissions.allow` array. Don't duplicate entries, don't remove or
|
||||
overwrite unrelated existing permissions in the file.
|
||||
5. Show the user exactly what was added (the four entries above with the
|
||||
real path filled in) before writing, so they can see precisely what
|
||||
they're approving.
|
||||
|
||||
Do **not** add anything beyond these four entries. In particular, do not
|
||||
whitelist writing to the vault or to the config file — those keep prompting
|
||||
for confirmation every time, by design, in `append`/`maintain` mode.
|
||||
|
||||
## 5. Make the helper scripts executable
|
||||
|
||||
```bash
|
||||
chmod +x <SKILL_DIR>/scripts/vault_index.sh <SKILL_DIR>/scripts/vault_search.sh <SKILL_DIR>/scripts/vault_backlinks.sh
|
||||
```
|
||||
|
||||
Note this modifies the shared `skills/obsidian-vault-kb/scripts/` files in
|
||||
the repo (via the resolved real path), not a plugin-local copy — that's
|
||||
expected, since the symlink means there is only one copy on disk.
|
||||
|
||||
## 6. Confirm and offer a test run
|
||||
|
||||
Summarize to the user:
|
||||
- Vault path(s) and mode that were configured
|
||||
- The exact four permission entries that were added, and where (project vs.
|
||||
user settings)
|
||||
- That nothing else was whitelisted — every other command, and any write
|
||||
into the vault, still prompts for confirmation
|
||||
|
||||
Then offer to run `<SKILL_DIR>/scripts/vault_index.sh` as a quick sanity
|
||||
check, if the user wants to see it working right away.
|
||||
Do **not** follow this with a test invocation of `vault_index.sh` — Claude
|
||||
Code loads permissions at session start and doesn't always pick up changes
|
||||
made by its own file edits within the same session, so an immediate test
|
||||
can prompt (or not) in a way that doesn't reliably indicate success either
|
||||
way. If a real command later still prompts, the fix is a fresh session, not
|
||||
re-running this setup — the configuration is already correctly saved.
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"_comment": "Template for this skill's Claude Code permissions. <SKILL_DIR> must be replaced with the absolute path where this plugin's skills/obsidian-vault-kb directory resolves to (i.e. the real, symlink-resolved path of skills/obsidian-vault-kb inside vendor/claude-code/plugins/obsidian-vault-kb/) before merging into settings.json. The /obsidian-vault-kb:setup command does this substitution automatically. Only the three read-only wrapper scripts are whitelisted, each pinned to its exact absolute path — no generic Bash(find:*), Bash(rg:*), Bash(cat:*), etc. The wrapper scripts themselves resolve the vault path only from ~/.agent-skills/obsidian-vault-kb/config.json and refuse to operate outside it, so whitelisting them does not grant filesystem access beyond the configured vault.",
|
||||
"_comment": "Reference only — setup.sh generates and writes these exact rules itself, so this file is no longer read during setup. Kept here for auditing/documentation: this is what /obsidian-vault-kb:setup will add to your permissions.allow, with <SKILL_DIR> substituted for the real, symlink-resolved skills/obsidian-vault-kb path. Only the three read-only wrapper scripts are whitelisted, each pinned to its exact absolute path — no generic Bash(find:*), Bash(rg:*), Bash(cat:*), etc. The wrapper scripts themselves resolve the vault path only from ~/.agent-skills/obsidian-vault-kb/config.json and refuse to operate outside it, so whitelisting them does not grant filesystem access beyond the configured vault.",
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(<SKILL_DIR>/scripts/vault_index.sh:*)",
|
||||
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
#!/usr/bin/env bash
|
||||
# One-shot setup for the obsidian-vault-kb plugin.
|
||||
#
|
||||
# Order matters here, deliberately: the permission rules are established
|
||||
# FIRST, against a stable path we control ourselves
|
||||
# (~/.agent-skills/obsidian-vault-kb/bin/), not against Claude Code's
|
||||
# internal plugin cache path (which changes on every plugin update).
|
||||
# Configuration (vaults, mode) comes after. Re-running this script after a
|
||||
# plugin update re-syncs the stable copy without the permission rules ever
|
||||
# needing to change again.
|
||||
#
|
||||
# Usage:
|
||||
# setup.sh --settings-scope (project|user) [--project-dir <path>]
|
||||
# --mode (read-only|append|maintain)
|
||||
# --vault <name>=<path> [--vault <name>=<path> ...]
|
||||
#
|
||||
# Prints a JSON summary of what changed to stdout at the end.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PLUGIN_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
SKILL_DIR="$(realpath "$PLUGIN_DIR/skills/obsidian-vault-kb")"
|
||||
|
||||
if ! command -v python3 >/dev/null 2>&1; then
|
||||
echo "Error: python3 is required." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- 1. Stable, self-controlled target location. Copy the three wrapper
|
||||
# scripts and their shared helper there — this is the ONLY location
|
||||
# permissions will ever point to. ---
|
||||
STABLE_DIR="$HOME/.agent-skills/obsidian-vault-kb/bin"
|
||||
mkdir -p "$STABLE_DIR"
|
||||
for f in vault_index.sh vault_search.sh vault_backlinks.sh _lib.sh; do
|
||||
cp "$SKILL_DIR/scripts/$f" "$STABLE_DIR/$f"
|
||||
done
|
||||
chmod +x "$STABLE_DIR/vault_index.sh" "$STABLE_DIR/vault_search.sh" "$STABLE_DIR/vault_backlinks.sh"
|
||||
|
||||
# Tilde-form path for permission rules and SKILL.md's example commands —
|
||||
# Claude Code matches Bash permission rules against the literal,
|
||||
# unexpanded command text, so a rule written with the resolved absolute
|
||||
# $HOME path (e.g. /home/hmk/...) does NOT match a command Claude typed as
|
||||
# "~/...", even though both point at the same file. Keep both sides in
|
||||
# tilde form so they actually match.
|
||||
TILDE_STABLE_DIR="~/.agent-skills/obsidian-vault-kb/bin"
|
||||
TILDE_CONFIG_FILE="~/.agent-skills/obsidian-vault-kb/config.json"
|
||||
|
||||
# --- 2. Parse arguments ---
|
||||
SETTINGS_SCOPE=""
|
||||
PROJECT_DIR="$PWD"
|
||||
MODE=""
|
||||
VAULTS=()
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--settings-scope) SETTINGS_SCOPE="$2"; shift 2 ;;
|
||||
--project-dir) PROJECT_DIR="$2"; shift 2 ;;
|
||||
--mode) MODE="$2"; shift 2 ;;
|
||||
--vault) VAULTS+=("$2"); shift 2 ;;
|
||||
*) echo "Error: unknown argument '$1'" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ "$SETTINGS_SCOPE" != "project" ] && [ "$SETTINGS_SCOPE" != "user" ]; then
|
||||
echo "Error: --settings-scope must be 'project' or 'user'" >&2
|
||||
exit 1
|
||||
fi
|
||||
case "$MODE" in
|
||||
read-only|append|maintain) ;;
|
||||
*) echo "Error: --mode must be read-only, append, or maintain" >&2; exit 1 ;;
|
||||
esac
|
||||
if [ "${#VAULTS[@]}" -eq 0 ]; then
|
||||
echo "Error: at least one --vault <name>=<path> is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$SETTINGS_SCOPE" = "project" ]; then
|
||||
SETTINGS_FILE="$PROJECT_DIR/.claude/settings.json"
|
||||
else
|
||||
SETTINGS_FILE="$HOME/.claude/settings.json"
|
||||
fi
|
||||
|
||||
CONFIG_DIR="$HOME/.agent-skills/obsidian-vault-kb"
|
||||
CONFIG_FILE="$CONFIG_DIR/config.json"
|
||||
mkdir -p "$CONFIG_DIR"
|
||||
|
||||
# --- 3. Write permissions FIRST, against the stable path from step 1 ---
|
||||
mkdir -p "$(dirname "$SETTINGS_FILE")"
|
||||
|
||||
python3 - "$SETTINGS_FILE" "$TILDE_STABLE_DIR" "$TILDE_CONFIG_FILE" << 'PYEOF'
|
||||
import json, os, sys
|
||||
|
||||
settings_file, tilde_stable_dir, tilde_config_file = sys.argv[1:4]
|
||||
|
||||
settings = {}
|
||||
if os.path.exists(settings_file):
|
||||
with open(settings_file) as f:
|
||||
settings = json.load(f)
|
||||
|
||||
perms = settings.setdefault("permissions", {})
|
||||
allow = perms.setdefault("allow", [])
|
||||
|
||||
new_rules = [
|
||||
f"Bash({tilde_stable_dir}/vault_index.sh:*)",
|
||||
f"Bash({tilde_stable_dir}/vault_search.sh:*)",
|
||||
f"Bash({tilde_stable_dir}/vault_backlinks.sh:*)",
|
||||
f"Read({tilde_config_file})",
|
||||
]
|
||||
for rule in new_rules:
|
||||
if rule not in allow:
|
||||
allow.append(rule)
|
||||
|
||||
with open(settings_file, "w") as f:
|
||||
json.dump(settings, f, indent=2)
|
||||
f.write("\n")
|
||||
PYEOF
|
||||
|
||||
# --- 4. THEN configuration: validate + write/merge vaults and mode ---
|
||||
VAULTS_JSON=$(python3 -c '
|
||||
import json, os, sys
|
||||
pairs = sys.argv[1:]
|
||||
vaults = []
|
||||
for p in pairs:
|
||||
name, path = p.split("=", 1)
|
||||
real = os.path.realpath(path)
|
||||
if not os.path.isdir(real):
|
||||
sys.stderr.write(f"Error: vault path {path!r} (for {name!r}) is not a directory.\n")
|
||||
sys.exit(1)
|
||||
vaults.append({"name": name, "path": real})
|
||||
print(json.dumps(vaults))
|
||||
' "${VAULTS[@]}")
|
||||
|
||||
python3 - "$CONFIG_FILE" "$VAULTS_JSON" "$MODE" << 'PYEOF'
|
||||
import json, os, sys
|
||||
config_file, new_vaults_json, mode = sys.argv[1], sys.argv[2], sys.argv[3]
|
||||
new_vaults = json.loads(new_vaults_json)
|
||||
|
||||
existing = {"vaults": [], "mode": mode}
|
||||
if os.path.exists(config_file):
|
||||
with open(config_file) as f:
|
||||
existing = json.load(f)
|
||||
existing["mode"] = mode
|
||||
|
||||
existing_by_name = {v["name"]: v for v in existing.get("vaults", [])}
|
||||
for v in new_vaults:
|
||||
existing_by_name[v["name"]] = v # upsert: new/updated path always wins
|
||||
existing["vaults"] = list(existing_by_name.values())
|
||||
|
||||
with open(config_file, "w") as f:
|
||||
json.dump(existing, f, indent=2)
|
||||
f.write("\n")
|
||||
PYEOF
|
||||
|
||||
# --- 5. Summary ---
|
||||
python3 - "$STABLE_DIR" "$SETTINGS_FILE" "$CONFIG_FILE" "$MODE" "$VAULTS_JSON" "$TILDE_STABLE_DIR" "$TILDE_CONFIG_FILE" << 'PYEOF'
|
||||
import json, sys
|
||||
stable_dir, settings_file, config_file, mode, vaults_json, tilde_stable_dir, tilde_config_file = sys.argv[1:8]
|
||||
print(json.dumps({
|
||||
"stable_script_location": stable_dir,
|
||||
"settings_file": settings_file,
|
||||
"config_file": config_file,
|
||||
"mode": mode,
|
||||
"vaults_registered": json.loads(vaults_json),
|
||||
"permission_rules_added": [
|
||||
f"Bash({tilde_stable_dir}/vault_index.sh:*)",
|
||||
f"Bash({tilde_stable_dir}/vault_search.sh:*)",
|
||||
f"Bash({tilde_stable_dir}/vault_backlinks.sh:*)",
|
||||
f"Read({tilde_config_file})"
|
||||
],
|
||||
"note": "The permission rules point at a stable location this script controls (~/.agent-skills/obsidian-vault-kb/bin/), not at Claude Code's internal plugin cache — so they survive future plugin updates without changing. They're written in tilde form (~/...) to match the literal, unexpanded command text Claude Code matches against, not the resolved absolute $HOME path. Permissions are written to disk now, but Claude Code loads permissions at session start and does not always pick up changes made by its own file edits within the same session — if a vault script call still prompts after this, start a fresh session rather than re-running setup."
|
||||
}, indent=2))
|
||||
PYEOF
|
||||
Reference in New Issue
Block a user