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:
@@ -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`
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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
47
.claude/scripts/vault.sh
Normal 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" "$@"
|
||||
@@ -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; }
|
||||
|
||||
@@ -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..."
|
||||
|
||||
Reference in New Issue
Block a user