#!/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 ] # --mode (read-only|append|maintain) # --vault = [--vault = ...] # # 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 = 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