diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 7bc6aeb..301e454 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -15,9 +15,11 @@ This repo is shared across multiple team members. **At every session start, BEFO "full_name": "Mike Swanson", "email": "mike@azcomputerguru.com", "role": "admin", - "machine": "" + "machine": "", + "vault_path": "" } ``` + Ask the user where the vault repo is cloned on this machine (e.g., `D:/vault`, `~/vault`, `/Users/howard/vault`). - Set local git config: `git config user.name ""` and `git config user.email ""` - Set git remote (read `gitea_username` from users.json): `git remote set-url origin https://@git.azcomputerguru.com/azcomputerguru/claudetools.git` - Add hostname to user's `known_machines` in users.json and commit. @@ -173,23 +175,22 @@ When user references previous work, use `/context` command. Never ask for info i ### Credential Access (SOPS Vault) -Always resolve vault path portably — never hardcode `D:/vault`: +Use the ClaudeTools vault wrapper — never hardcode the vault path: ```bash -VAULT_SH="" -for _c in "D:/vault/scripts/vault.sh" "$HOME/vault/scripts/vault.sh" "/d/vault/scripts/vault.sh" "$HOME/.vault/scripts/vault.sh"; do - [[ -f "$_c" ]] && VAULT_SH="$_c" && break -done -[[ -z "$VAULT_SH" ]] && { echo "ERROR: vault not found" >&2; exit 1; } +# CLAUDETOOLS_ROOT is the repo root (D:\claudetools on Windows, ~/claudetools on Mac/Linux) +VAULT="$CLAUDETOOLS_ROOT/.claude/scripts/vault.sh" -bash "$VAULT_SH" search "keyword" # Search without decrypting -bash "$VAULT_SH" get-field # Get specific field -bash "$VAULT_SH" get # Decrypt full entry -bash "$VAULT_SH" list # List all entries +bash "$VAULT" search "keyword" # Search without decrypting +bash "$VAULT" get-field # Get specific field +bash "$VAULT" get # Decrypt full entry +bash "$VAULT" list # List all entries ``` -Vault repo: cloned at `D:\vault` (Windows) or `~/vault` (Mac/Linux) — set `VAULT_PATH` env var to override. -Structure: `infrastructure/`, `clients/`, `services/`, `projects/`, `msp-tools/` +The wrapper reads `vault_path` from `.claude/identity.json` (per-machine, gitignored). +Each machine sets its own vault path there — no hardcoded paths in any shared file. + +Vault structure: `infrastructure/`, `clients/`, `services/`, `projects/`, `msp-tools/` **1Password fallback:** service account token in `infrastructure/1password-service-account.sops.yaml` diff --git a/.claude/ONBOARDING.md b/.claude/ONBOARDING.md index c4d745b..f01140e 100644 --- a/.claude/ONBOARDING.md +++ b/.claude/ONBOARDING.md @@ -65,24 +65,37 @@ Without `/save`, you'd lose everything when a session ends. Without `/sync`, you ## The SOPS vault (how credentials work) -We store ALL credentials in an encrypted vault at `D:\vault\` (separate git repo). Files are YAML encrypted with age/SOPS. Claude can decrypt them on the fly. +We store ALL credentials in an encrypted vault (separate git repo). Files are YAML encrypted with age/SOPS. Claude can decrypt them on the fly. **How Claude accesses a credential:** ```bash -bash D:/vault/scripts/vault.sh get-field clients/dataforth/ad2.sops.yaml credentials.password +# Always via the ClaudeTools wrapper — never a hardcoded path +bash "$CLAUDETOOLS_ROOT/.claude/scripts/vault.sh" get-field clients/dataforth/ad2.sops.yaml credentials.password ``` **Why this matters:** - We never hardcode passwords in scripts or session logs (they're vault references) - The vault syncs across machines via Gitea (same as claudetools) -- Encryption uses an age key at `%APPDATA%\sops\age\keys.txt` — this key needs to be on each machine that decrypts +- Encryption uses an age key — this key needs to be on each machine that decrypts -**Your machine needs the age key.** Mike will give you the key file. Drop it at: -``` -C:\Users\\AppData\Roaming\sops\age\keys.txt -``` +**Setup required on each machine:** -Without this file, vault commands fail. Everything else works fine. +1. **Clone the vault repo** somewhere convenient (e.g., `~/vault` on Mac/Linux, `D:\vault` on Windows) + +2. **Add `vault_path` to `.claude/identity.json`** (created during onboarding): + ```json + { + "user": "howard", + "vault_path": "/Users/howard/vault" + } + ``` + This is the only place the path lives — no hardcoded paths in any shared file. + +3. **Install your age key.** Mike will give you the key file. Drop it at: + - **Windows:** `C:\Users\\AppData\Roaming\sops\age\keys.txt` + - **Mac/Linux:** `~/.config/sops/age/keys.txt` + +Without the age key, vault commands fail. Everything else works fine. --- diff --git a/.claude/commands/syncro.md b/.claude/commands/syncro.md index 8d37d0a..a93e7f5 100644 --- a/.claude/commands/syncro.md +++ b/.claude/commands/syncro.md @@ -30,20 +30,15 @@ When invoked, use the Syncro REST API via `curl`. All requests include `?api_key ### Get API key ```bash -# Portable vault resolver — works on Windows (D:/vault), Mac (~/.vault or ~/vault), Linux -VAULT_SH="" -for _c in "D:/vault/scripts/vault.sh" "$HOME/vault/scripts/vault.sh" "/d/vault/scripts/vault.sh" "$HOME/.vault/scripts/vault.sh"; do - [[ -f "$_c" ]] && VAULT_SH="$_c" && break -done -[[ -z "$VAULT_SH" ]] && { echo "ERROR: vault.sh not found" >&2; exit 1; } - -API_KEY=$(bash "$VAULT_SH" get-field msp-tools/syncro.sops.yaml credentials.credential) +# Vault path comes from .claude/identity.json (per-machine) via the ClaudeTools wrapper +VAULT="$CLAUDETOOLS_ROOT/.claude/scripts/vault.sh" +API_KEY=$(bash "$VAULT" get-field msp-tools/syncro.sops.yaml credentials.credential) BASE="https://computerguru.syncromsp.com/api/v1" ``` If `vault.sh get-field` fails (yq not installed), fall back to: ```bash -VAULT_ROOT=$(dirname "$(dirname "$VAULT_SH")") +VAULT_ROOT=$(bash "$VAULT" get msp-tools/syncro.sops.yaml 2>/dev/null | head -1 || python3 -c "import json; print(json.load(open('$CLAUDETOOLS_ROOT/.claude/identity.json'))['vault_path'])") API_KEY=$(sops -d "$VAULT_ROOT/msp-tools/syncro.sops.yaml" | py -c "import sys,yaml; print(yaml.safe_load(sys.stdin)['credentials']['credential'])") ``` diff --git a/.claude/scripts/vault.sh b/.claude/scripts/vault.sh new file mode 100644 index 0000000..fe2a700 --- /dev/null +++ b/.claude/scripts/vault.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# vault.sh — ClaudeTools wrapper for the SOPS vault. +# +# Reads vault_path from .claude/identity.json (per-machine, gitignored). +# Delegates all arguments to the real vault.sh in that directory. +# +# Usage (from any directory): +# bash "$(git -C "$(dirname "${BASH_SOURCE[0]}")" rev-parse --show-toplevel)/.claude/scripts/vault.sh" get-field +# +# Or set CLAUDETOOLS_ROOT and call directly: +# bash "$CLAUDETOOLS_ROOT/.claude/scripts/vault.sh" get-field + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CLAUDETOOLS_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +IDENTITY_FILE="$CLAUDETOOLS_ROOT/.claude/identity.json" + +if [[ ! -f "$IDENTITY_FILE" ]]; then + echo "[ERROR] .claude/identity.json not found at $IDENTITY_FILE" >&2 + echo " Run onboarding to create it, or add vault_path manually." >&2 + exit 1 +fi + +# Extract vault_path from identity.json using python (available on all platforms) +VAULT_ROOT="" +for py in py python3 python; do + if command -v "$py" >/dev/null 2>&1; then + VAULT_ROOT=$("$py" -c "import json,sys; d=json.load(open('$IDENTITY_FILE')); print(d.get('vault_path',''))" 2>/dev/null) && break + fi +done + +if [[ -z "$VAULT_ROOT" ]]; then + echo "[ERROR] vault_path not set in $IDENTITY_FILE" >&2 + echo " Add: \"vault_path\": \"/path/to/vault\"" >&2 + exit 1 +fi + +REAL_VAULT_SH="$VAULT_ROOT/scripts/vault.sh" + +if [[ ! -f "$REAL_VAULT_SH" ]]; then + echo "[ERROR] vault.sh not found at $REAL_VAULT_SH" >&2 + echo " Check vault_path in $IDENTITY_FILE" >&2 + exit 1 +fi + +exec bash "$REAL_VAULT_SH" "$@" diff --git a/.claude/skills/remediation-tool/scripts/get-token.sh b/.claude/skills/remediation-tool/scripts/get-token.sh index 6e0c0b1..acf00a3 100644 --- a/.claude/skills/remediation-tool/scripts/get-token.sh +++ b/.claude/skills/remediation-tool/scripts/get-token.sh @@ -81,13 +81,22 @@ if [[ -f "$CACHE_FILE" ]] && [[ $(find "$CACHE_FILE" -mmin -55 2>/dev/null) ]]; exit 0 fi -# Locate vault repo — candidates cover Windows (D:/vault), Git Bash (/d/vault), -# Mac/Linux ($HOME/vault), and optional override via VAULT_PATH env var. -VAULT_ROOT="" -for candidate in "${VAULT_PATH:-}" "D:/vault" "$HOME/vault" "/d/vault" "$HOME/.vault"; do - [[ -n "$candidate" && -d "$candidate" ]] && VAULT_ROOT="$candidate" && break -done -[[ -z "$VAULT_ROOT" ]] && { echo "ERROR: SOPS vault not found (tried D:/vault ~/vault /d/vault ~/.vault; set VAULT_PATH to override)" >&2; exit 3; } +# Locate vault repo via .claude/identity.json (per-machine, gitignored). +# Falls back to VAULT_PATH env var if set. +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CLAUDETOOLS_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" +IDENTITY_FILE="$CLAUDETOOLS_ROOT/.claude/identity.json" + +VAULT_ROOT="${VAULT_PATH:-}" +if [[ -z "$VAULT_ROOT" && -f "$IDENTITY_FILE" ]]; then + for py in py python3 python; do + if command -v "$py" >/dev/null 2>&1; then + VAULT_ROOT=$("$py" -c "import json; print(json.load(open('$IDENTITY_FILE')).get('vault_path',''))" 2>/dev/null) && break + fi + done +fi +[[ -z "$VAULT_ROOT" ]] && { echo "ERROR: vault_path not set in $IDENTITY_FILE and VAULT_PATH env var not set" >&2; exit 3; } +[[ ! -d "$VAULT_ROOT" ]] && { echo "ERROR: vault not found at $VAULT_ROOT (check vault_path in $IDENTITY_FILE)" >&2; exit 3; } SOPS_FILE="$VAULT_ROOT/$VAULT_PATH" [[ ! -f "$SOPS_FILE" ]] && { echo "ERROR: vault file not found: $SOPS_FILE" >&2; exit 3; } diff --git a/.claude/skills/remediation-tool/scripts/patch-tenant-admin-manifest.sh b/.claude/skills/remediation-tool/scripts/patch-tenant-admin-manifest.sh index 5218c75..a85ff35 100644 --- a/.claude/skills/remediation-tool/scripts/patch-tenant-admin-manifest.sh +++ b/.claude/skills/remediation-tool/scripts/patch-tenant-admin-manifest.sh @@ -17,11 +17,20 @@ TENANT_ADMIN_APP_ID="709e6eed-0711-4875-9c44-2d3518c47063" GRAPH_RESOURCE_APP_ID="00000003-0000-0000-c000-000000000000" ROLE_MGMT_PERMISSION_ID="9e3f62cf-ca93-4989-b6ce-bf83c28f9fe8" -VAULT_ROOT="" -for candidate in "${VAULT_PATH:-}" "D:/vault" "$HOME/vault" "/d/vault" "$HOME/.vault"; do - [[ -n "$candidate" && -d "$candidate" ]] && VAULT_ROOT="$candidate" && break -done -[[ -z "$VAULT_ROOT" ]] && { echo "[ERROR] SOPS vault not found (tried D:/vault ~/vault /d/vault ~/.vault; set VAULT_PATH to override)" >&2; exit 3; } +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CLAUDETOOLS_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" +IDENTITY_FILE="$CLAUDETOOLS_ROOT/.claude/identity.json" + +VAULT_ROOT="${VAULT_PATH:-}" +if [[ -z "$VAULT_ROOT" && -f "$IDENTITY_FILE" ]]; then + for py in py python3 python; do + if command -v "$py" >/dev/null 2>&1; then + VAULT_ROOT=$("$py" -c "import json; print(json.load(open('$IDENTITY_FILE')).get('vault_path',''))" 2>/dev/null) && break + fi + done +fi +[[ -z "$VAULT_ROOT" ]] && { echo "[ERROR] vault_path not set in $IDENTITY_FILE and VAULT_PATH env var not set" >&2; exit 3; } +[[ ! -d "$VAULT_ROOT" ]] && { echo "[ERROR] vault not found at $VAULT_ROOT (check vault_path in $IDENTITY_FILE)" >&2; exit 3; } # ── Step 1: Get Management app client secret ────────────────────────────────── echo "[INFO] Reading Management app secret from vault..."