fix: vault path from per-machine identity.json, not hardcoded paths

- Add .claude/scripts/vault.sh wrapper (reads vault_path from identity.json)
- get-token.sh + patch-tenant-admin-manifest.sh read identity.json for vault root
- syncro.md uses wrapper via CLAUDETOOLS_ROOT
- CLAUDE.md + ONBOARDING.md document the pattern and prompt for vault_path on onboarding
- identity.json now includes vault_path (D:/vault on DESKTOP-0O8A1RL)

Howard and Mac need vault_path added to their identity.json after pulling.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-21 19:01:27 -07:00
parent 0a7cd6b778
commit a86df117d2
6 changed files with 116 additions and 42 deletions

View File

@@ -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": "<HOSTNAME>"
"machine": "<HOSTNAME>",
"vault_path": "<absolute path to vault repo on this machine>"
}
```
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 "<full_name>"` and `git config user.email "<email>"`
- Set git remote (read `gitea_username` from users.json): `git remote set-url origin https://<gitea_username>@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 <path> <field> # Get specific field
bash "$VAULT_SH" get <path> # Decrypt full entry
bash "$VAULT_SH" list # List all entries
bash "$VAULT" search "keyword" # Search without decrypting
bash "$VAULT" get-field <path> <field> # Get specific field
bash "$VAULT" get <path> # 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`

View File

@@ -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\<you>\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\<you>\AppData\Roaming\sops\age\keys.txt`
- **Mac/Linux:** `~/.config/sops/age/keys.txt`
Without the age key, vault commands fail. Everything else works fine.
---

View File

@@ -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'])")
```

47
.claude/scripts/vault.sh Normal file
View File

@@ -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 <path> <field>
#
# Or set CLAUDETOOLS_ROOT and call directly:
# bash "$CLAUDETOOLS_ROOT/.claude/scripts/vault.sh" get-field <path> <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" "$@"

View File

@@ -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; }

View File

@@ -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..."