harness: fleet-wide functional-error + correction + friction logging

Add .claude/scripts/log-skill-error.sh — the canonical agent error log helper
(writes errorlog.md in DATE | MACHINE | skill | [type] error format, soft-fails).
Three categories: execution failures (default), user corrections (--correction),
and preventable self-inflicted friction (--friction; cite ref= when it repeats a
documented gotcha). Goal: stop paying tokens twice for the same avoidable mistake.

- CLAUDE.md: make logging mandatory for all skills + corrections + friction.
- skill-creator: new skills must wire in the helper (guidance + checklist).
- Retrofit every skill script's genuine failure branches to call the helper
  (b2/bitdefender/mailprotector/packetdial/coord python CLIs; remediation-tool
  + onboard365 bash; vault, rmm-auth, post-bot-alert, agy, grok, 1password,
  run-onboarding-diagnostic). Handled conditions + self-tests left alone.
- errorlog.md: broaden header to cover skills + harness + corrections; seed this
  session's corrections (INKY, Mail.Send token-audience, omnibox-strictness) and
  friction (git-bash /tmp, env-persistence, argv-limit, PowerShell var-case).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-15 11:39:43 -07:00
parent 927a06a0cf
commit 9960da5f9a
29 changed files with 388 additions and 36 deletions

View File

@@ -43,7 +43,9 @@ production, data-loss. Detail: EXTENDED + `.claude/OLLAMA.md`.
- **SSH:** system OpenSSH (`C:\Windows\System32\OpenSSH\ssh.exe`), never Git-for-Windows SSH.
- **Data integrity:** never placeholder/fake data — check vault, wiki, or ask.
- **Hard-to-reverse or outward-facing actions:** confirm first (per-action, per-session).
- **Error logging:** when a task errors during execution (failed command, skill, or tool call), append a brief entry to `errorlog.md` (repo root) — date, machine (from `identity.json`), the command/skill that failed, and the error. One line or two; just enough to spot patterns. These feed skill + harness improvements. Don't log expected/handled conditions, only genuine failures.
- **Error logging (mandatory, all skills):** when a task or skill hits a GENUINE functional error during execution (failed command, API/auth failure, unexpected API response, tool call), record it to `errorlog.md` (repo root) via the canonical helper — never hand-format: `bash .claude/scripts/log-skill-error.sh "<skill/command>" "<brief error>" [--context "k=v ..."]`. It stamps UTC date + machine (from `identity.json`) and inserts in the standard `YYYY-MM-DD | MACHINE | skill | error` format (newest on top) for later skill **linting**, and soft-fails so it never breaks the caller. **Every skill MUST call it at its failure branches**; you (main loop) call it after any skill/command genuinely fails. Do NOT log expected/handled conditions (no search matches, no unread messages, a user declining a prompt) — only real failures worth spotting a pattern. Python skills shell out to the same helper.
- **Log user corrections too (`--correction`):** when the user CORRECTS an improper assumption or wrong approach you took (e.g. "don't use INKY unless onboarded", "EXO already has Mail.Send", "I don't need an exact match"), log it: `bash .claude/scripts/log-skill-error.sh "<skill/context>" "assumed X; correct is Y" --correction`. These are the highest-value entries — they surface recurring bad assumptions so we can train them out of the system. If the correction is a durable preference, ALSO save it as a `feedback` memory (the two are complementary: memory fixes future behavior, errorlog tracks the pattern for linting).
- **Log preventable friction too (`--friction`):** any time you waste tokens on a preventable, repeatable self-inflicted error — harness/env/tool misuse (Git-Bash `/tmp` path mismatch, shell env not persisting between Bash calls, passing huge args on the command line, PowerShell var case-collisions, etc.) — log it: `bash .claude/scripts/log-skill-error.sh "<context e.g. bash/env>" "what wasted tokens + the fix" --friction [--context "ref=<memory-or-rule>"]`. **If it repeats something already in memory or CLAUDE.md, that's the highest-value entry** — it means a rule/memory isn't working; cite the ref. This log is the corpus we lint to build better CLAUDE.md rules and to clean stale/misleading memory. Goal: stop paying twice for the same mistake.
- **Windows:** ensure `bash` resolves to Git-for-Windows MSYS bash, not the WSL stub; write
`.claude/current-mode` with a relative/forward-slash path only (never a backslash Windows
path). Detail + fixes: EXTENDED.

View File

@@ -78,7 +78,11 @@ if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then
ENV_FILE="$ROOT/projects/discord-bot/.env"
[ -f "$ENV_FILE" ] && TOKEN="$(grep -iE '^[[:space:]]*DISCORD_TOKEN[[:space:]]*=' "$ENV_FILE" | head -1 | sed -E 's/^[^=]*=[[:space:]]*//; s/^["'"'"']//; s/["'"'"'][[:space:]]*$//')"
fi
if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then echo "[ERROR] no bot token (vault + .env both empty)" >&2; exit 2; fi
if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then
echo "[ERROR] no bot token (vault + .env both empty)" >&2
bash "$ROOT/.claude/scripts/log-skill-error.sh" "discord-dm" "no Discord bot token (vault projects/discord-bot/bot-token + .env both empty)" >/dev/null 2>&1
exit 2
fi
auth=(-H "Authorization: Bot ${TOKEN}" -H "Content-Type: application/json" -H "User-Agent: ${UA}")
@@ -87,7 +91,11 @@ if [ "$MODE" = "dm" ]; then
DM="$(printf '%s' "$(jq -nc --arg r "$TARGET" '{recipient_id:$r}')" | \
curl -s -m 15 "${auth[@]}" -X POST "$API/users/@me/channels" --data-binary @-)"
CHID="$(printf '%s' "$DM" | jq -r '.id // empty' 2>/dev/null)"
if [ -z "$CHID" ]; then echo "[ERROR] could not open DM channel for $LABEL: $DM" >&2; exit 3; fi
if [ -z "$CHID" ]; then
echo "[ERROR] could not open DM channel for $LABEL: $DM" >&2
bash "$ROOT/.claude/scripts/log-skill-error.sh" "discord-dm" "failed to open DM channel for $LABEL" --context "resp=${DM:0:80}" >/dev/null 2>&1
exit 3
fi
TARGET="$CHID"
fi
@@ -103,4 +111,5 @@ if [ "$HTTP" = "200" ]; then
exit 0
fi
echo "[ERROR] discord-dm: Discord returned ${HTTP:-no-response}${BODY}" >&2
bash "$ROOT/.claude/scripts/log-skill-error.sh" "discord-dm" "Discord send to $LABEL failed" --context "http=${HTTP:-none} resp=${BODY:0:80}" >/dev/null 2>&1
exit 3

View File

@@ -0,0 +1,89 @@
#!/usr/bin/env bash
# log-skill-error.sh — append an entry to errorlog.md in the canonical format,
# for later linting that feeds skill fixes, CLAUDE.md rules, and memory cleanup.
#
# Despite the name this is the GENERAL agent error/correction/friction log — it
# captures three things (see --type below):
# 1. skill/command FUNCTIONAL failures (API/auth/unexpected-response/bad-exit)
# 2. user CORRECTIONS of an improper assumption I made (--correction)
# 3. preventable self-inflicted FRICTION that wasted tokens (--friction) —
# harness/env/tool misuse, ESPECIALLY a repeat of an already-documented
# gotcha (that means a rule or memory isn't working and needs strengthening)
#
# Do NOT call it for expected/handled conditions (a search with no matches, a
# "no unread messages", a user declining a prompt) — only real, preventable,
# pattern-worthy events.
#
# Usage:
# bash log-skill-error.sh <skill-or-command> "<brief error>"
# echo "<brief error>" | bash log-skill-error.sh <skill-or-command>
# bash log-skill-error.sh <skill> "<error>" --context "op=send id=123 http=403"
# bash log-skill-error.sh <skill/context> "<what I wrongly assumed + the correction>" --correction
#
# Categories (all feed the lint that improves skills, CLAUDE.md, and memory):
# (default) execution failure — API/auth failure, unexpected response, bad exit.
# --correction — the USER corrected an improper assumption/approach I made.
# --friction — preventable self-inflicted error that wasted tokens (harness/env/
# tool misuse). If it repeats a documented gotcha, note it in
# --context (e.g. ref=feedback_tmp_path_windows) — that's the signal
# a rule/memory needs strengthening.
# (--type <other> also supported; tags the error column as [<type>].)
# bash log-skill-error.sh <context> "<what wasted tokens + the fix>" --friction --context "ref=<memory>"
#
# Writes: YYYY-MM-DD | MACHINE | <skill> | [<type>] <error> [ctx: <context>]
# (newest entry inserted at the top, just under the append marker).
#
# Soft-fail by design: this NEVER breaks the caller. Missing log, missing jq,
# empty message -> prints a [WARN] to stderr and exits 0.
set -u
ROOT="${CLAUDETOOLS_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
SKILL="${1:-unknown}"; shift || true
CONTEXT=""
ETYPE="" # "" / exec = execution failure; "correction" = user corrected a bad assumption
ARGS=()
while [ $# -gt 0 ]; do
case "$1" in
--context) CONTEXT="${2:-}"; shift 2;;
--type) ETYPE="${2:-}"; shift 2;;
--correction) ETYPE="correction"; shift;;
--friction) ETYPE="friction"; shift;;
*) ARGS+=("$1"); shift;;
esac
done
MSG="${ARGS[*]:-}"
if [ -z "$MSG" ] && [ ! -t 0 ]; then MSG="$(cat)"; fi
if [ -z "$MSG" ]; then echo "[WARN] log-skill-error: empty message, nothing logged" >&2; exit 0; fi
LOG="$ROOT/errorlog.md"
if [ ! -f "$LOG" ]; then echo "[WARN] log-skill-error: $LOG not found" >&2; exit 0; fi
DATE="$(date -u +%F)"
IDF="$ROOT/.claude/identity.json"
MACHINE=""
if command -v jq >/dev/null 2>&1 && [ -f "$IDF" ]; then
MACHINE="$(jq -r '.machine_name // .hostname // empty' "$IDF" 2>/dev/null)"
fi
[ -z "$MACHINE" ] && MACHINE="$(hostname 2>/dev/null || echo unknown)"
# normalize whitespace/newlines so each entry is one line
MSG="$(printf '%s' "$MSG" | tr '\n' ' ' | sed 's/[[:space:]]\{1,\}/ /g; s/^ //; s/ $//')"
[ -n "$CONTEXT" ] && MSG="$MSG [ctx: $CONTEXT]"
# Tag non-execution categories at the start of the error column for easy linting
# (e.g. grep "\[correction\]" errorlog.md to surface improper-assumption patterns).
if [ -n "$ETYPE" ] && [ "$ETYPE" != "exec" ]; then MSG="[$ETYPE] $MSG"; fi
ENTRY="$DATE | $MACHINE | $SKILL | $MSG"
MARK="<!-- Append entries below this line -->"
TMP="$LOG.tmp.$$"
if awk -v entry="$ENTRY" -v mark="$MARK" '
{ print }
($0==mark && !done) { print ""; print entry; done=1 }
END { if (!done) { print ""; print entry } } # marker missing -> append at end
' "$LOG" > "$TMP" 2>/dev/null && mv "$TMP" "$LOG" 2>/dev/null; then
echo "[OK] logged skill error to errorlog.md ($SKILL)"
else
rm -f "$TMP" 2>/dev/null
echo "[WARN] log-skill-error: could not write $LOG" >&2
fi
exit 0

View File

@@ -86,4 +86,8 @@ if [ "$HTTP" = "200" ]; then
fi
echo "[WARNING] post-bot-alert: Discord returned ${HTTP:-no-response}${BODY}" >&2
# Log the Discord POST failure (non-200 / unreachable) once. Do NOT route this
# through post-bot-alert itself — that would recurse; log-skill-error.sh only
# writes to errorlog.md. Soft-fail preserved: this never changes the exit 0.
bash "$ROOT/.claude/scripts/log-skill-error.sh" "post-bot-alert" "Discord POST failed (non-200/unreachable)" --context "channel=${CHANNEL_NAME} http=${HTTP:-none} resp=${BODY:0:80}" >/dev/null 2>&1 || true
exit 0

View File

@@ -11,19 +11,27 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
IDENTITY_FILE="$REPO_ROOT/.claude/identity.json"
# Functional-error logger. MUST stay silent on stdout (this script's stdout is
# eval'd by the caller) — log-skill-error.sh prints only to stderr, and we
# redirect everything to /dev/null to be safe.
_logerr() { bash "$REPO_ROOT/.claude/scripts/log-skill-error.sh" "rmm-auth" "$@" >/dev/null 2>&1 || true; }
if [ ! -f "$IDENTITY_FILE" ]; then
_logerr "identity.json not found; RMM auth cannot resolve vault" --context "path=$IDENTITY_FILE"
echo "export TOKEN=''; export RMM=''; export REPO_ROOT=''; echo '[ERROR] identity.json not found' >&2"
exit 1
fi
VAULT_PATH=$(jq -r '.vault_path // empty' "$IDENTITY_FILE")
if [ -z "$VAULT_PATH" ]; then
_logerr "vault_path not in identity.json; RMM auth failed" --context "path=$IDENTITY_FILE"
echo "export TOKEN=''; export RMM=''; export REPO_ROOT=''; echo '[ERROR] vault_path not in identity.json' >&2"
exit 1
fi
VAULT_SH="$VAULT_PATH/scripts/vault.sh"
if [ ! -f "$VAULT_SH" ]; then
_logerr "vault.sh not found at resolved vault_path; RMM auth failed" --context "path=$VAULT_SH"
echo "export TOKEN=''; export RMM=''; export REPO_ROOT=''; echo '[ERROR] vault.sh not found at $VAULT_SH' >&2"
exit 1
fi
@@ -35,6 +43,7 @@ RMM_EMAIL=$(bash "$VAULT_SH" get-field infrastructure/gururmm-server.sops.yaml c
RMM_PASS=$(bash "$VAULT_SH" get-field infrastructure/gururmm-server.sops.yaml credentials.gururmm-api.admin-password 2>/dev/null)
if [ -z "$RMM_EMAIL" ] || [ -z "$RMM_PASS" ]; then
_logerr "vault read of GuruRMM API credentials failed (empty email/password)" --context "entry=infrastructure/gururmm-server.sops.yaml"
echo "export TOKEN=''; export RMM=''; export REPO_ROOT=''; echo '[ERROR] Failed to get RMM credentials from vault' >&2"
exit 1
fi
@@ -45,6 +54,7 @@ JWT=$(curl -s -X POST "$RMM_URL/api/auth/login" -H "Content-Type: application/js
TOKEN=$(echo "$JWT" | jq -r '.token // empty')
if [ -z "$TOKEN" ]; then
_logerr "RMM login failed (no token returned from /api/auth/login)" --context "url=$RMM_URL resp=${JWT:0:80}"
echo "export TOKEN=''; export RMM=''; export REPO_ROOT=''; echo '[ERROR] RMM login failed: $JWT' >&2"
exit 1
fi

View File

@@ -37,9 +37,17 @@ if [ -z "$QUERY" ] && [ -z "$CLIENT" ] && [ "$LISTC" -eq 0 ]; then
fi
eval "$(bash "$ROOT/.claude/scripts/rmm-auth.sh" 2>/dev/null)" >/dev/null
if [ -z "${TOKEN:-}" ] || [ -z "${RMM:-}" ]; then echo "[ERROR] RMM auth failed (see rmm-auth.sh)" >&2; exit 1; fi
if [ -z "${TOKEN:-}" ] || [ -z "${RMM:-}" ]; then
echo "[ERROR] RMM auth failed (see rmm-auth.sh)" >&2
bash "$ROOT/.claude/scripts/log-skill-error.sh" "rmm-search" "RMM auth failed via rmm-auth.sh (no TOKEN/RMM)" >/dev/null 2>&1
exit 1
fi
AGENTS=$(curl -s "$RMM/api/agents" -H "Authorization: Bearer $TOKEN")
if [ -z "$AGENTS" ] || [ "${AGENTS:0:1}" != "[" ]; then echo "[ERROR] could not fetch agents: ${AGENTS:0:160}" >&2; exit 1; fi
if [ -z "$AGENTS" ] || [ "${AGENTS:0:1}" != "[" ]; then
echo "[ERROR] could not fetch agents: ${AGENTS:0:160}" >&2
bash "$ROOT/.claude/scripts/log-skill-error.sh" "rmm-search" "GET /api/agents returned non-array/empty" --context "resp=${AGENTS:0:80}" >/dev/null 2>&1
exit 1
fi
# Pipe agents on stdin (payload too large for argv on Windows); flags via env.
printf '%s' "$AGENTS" | QUERY="$QUERY" CLIENT="$CLIENT" ONLINE="$ONLINE" JSON="$JSON" LISTC="$LISTC" LIMIT="$LIMIT" \

View File

@@ -37,6 +37,11 @@ PROBE="$SCRIPT_DIR/onboarding-diagnostic.ps1"
ALERT="$REPO_ROOT/.claude/scripts/post-bot-alert.sh"
RMM="http://172.16.3.30:3001"
# Functional-error logger (skill name "rmm-diagnose"). Logs genuine operational
# failures (auth, vault, dispatch) — NOT the RED/AMBER/GREEN diagnostic grade,
# which is a normal by-design result. Soft-fails; never breaks the run.
_logerr() { bash "$REPO_ROOT/.claude/scripts/log-skill-error.sh" "rmm-diagnose" "$@" >/dev/null 2>&1 || true; }
if [ ! -f "$PROBE" ]; then
echo "[ERROR] Probe script not found: $PROBE" >&2
exit 1
@@ -65,6 +70,7 @@ RMM_PASS="$(bash "$VAULT" get-field infrastructure/gururmm-server.sops.yaml cred
if [ -z "$RMM_EMAIL" ] || [ -z "$RMM_PASS" ] || [ "$RMM_EMAIL" = "null" ]; then
echo "[ERROR] Could not read GuruRMM credentials from vault (infrastructure/gururmm-server.sops.yaml)" >&2
_logerr "vault read of GuruRMM credentials failed (empty/null)" --context "entry=infrastructure/gururmm-server.sops.yaml"
exit 1
fi
@@ -75,6 +81,7 @@ TOKEN="$(curl -s -m 30 -X POST "$RMM/api/auth/login" \
if [ -z "$TOKEN" ]; then
echo "[ERROR] RMM login failed (no token returned)" >&2
_logerr "RMM login failed (no token returned from /api/auth/login)" --context "url=$RMM"
exit 1
fi
echo "[OK] Authenticated to GuruRMM"
@@ -85,6 +92,7 @@ echo "[OK] Authenticated to GuruRMM"
AGENTS="$(curl -s -m 30 "$RMM/api/agents" -H "Authorization: Bearer $TOKEN")"
if [ -z "$AGENTS" ] || ! echo "$AGENTS" | jq -e 'type=="array"' >/dev/null 2>&1; then
echo "[ERROR] Could not retrieve agent list" >&2
_logerr "GET /api/agents returned non-array/empty" --context "resp=${AGENTS:0:80}"
exit 1
fi
@@ -263,6 +271,7 @@ PS
CH_STATUS="$(echo "$CH_RESULT" | jq -r '.status')"
if [ "$CH_STATUS" != "completed" ]; then
echo "[ERROR] Chunk $IDX upload failed: status=$CH_STATUS stderr=$(echo "$CH_RESULT" | jq -r '.stderr' | head -c 200)" >&2
_logerr "probe chunk upload failed" --context "host=$AGENT_HOST idx=$IDX/$N_CHUNKS status=$CH_STATUS"
exit 1
fi
echo "[OK] Uploaded chunk $IDX/$N_CHUNKS"
@@ -288,7 +297,7 @@ try {
}
PS
RESULT="$(dispatch_one "$RUN_SCRIPT" "$EXEC_TIMEOUT")" || { echo "[ERROR] Probe execution dispatch failed" >&2; exit 1; }
RESULT="$(dispatch_one "$RUN_SCRIPT" "$EXEC_TIMEOUT")" || { echo "[ERROR] Probe execution dispatch failed" >&2; _logerr "probe execution dispatch failed" --context "host=$AGENT_HOST agent=$AGENT_ID"; exit 1; }
CMD_ID="$(cat "$WORK_DIR/last_cmd_id" 2>/dev/null || echo unknown)"
FINAL_STATUS="$(echo "$RESULT" | jq -r '.status // empty')"
@@ -317,6 +326,7 @@ if [ -z "$DIAG_JSON" ] || ! echo "$DIAG_JSON" | jq -e '.host' >/dev/null 2>&1; t
fi
echo "--- stdout (first 60 lines) ---" >&2
printf '%s\n' "$STDOUT" | head -60 >&2
_logerr "could not extract valid diagnostic JSON from probe output" --context "host=$AGENT_HOST status=$FINAL_STATUS exit=$EXIT_CODE"
exit 1
fi

View File

@@ -16,9 +16,12 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CLAUDETOOLS_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
IDENTITY_FILE="$CLAUDETOOLS_ROOT/.claude/identity.json"
_logerr() { bash "$CLAUDETOOLS_ROOT/.claude/scripts/log-skill-error.sh" "vault" "$@" >/dev/null 2>&1 || true; }
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
_logerr "identity.json not found; vault read cannot resolve vault_path" --context "path=$IDENTITY_FILE"
exit 1
fi
@@ -40,6 +43,7 @@ fi
if [[ -z "$VAULT_ROOT" ]]; then
echo "[ERROR] vault_path not set in $IDENTITY_FILE" >&2
echo " Add: \"vault_path\": \"/path/to/vault\"" >&2
_logerr "vault_path not set in identity.json; vault read failed" --context "path=$IDENTITY_FILE"
exit 1
fi
@@ -48,6 +52,7 @@ 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
_logerr "real vault.sh not found at resolved vault_path; vault read failed" --context "path=$REAL_VAULT_SH"
exit 1
fi

View File

@@ -14,6 +14,10 @@
set -euo pipefail
# Functional-error logger (skill name "1password"); 4 levels up to the ClaudeTools repo.
CLAUDETOOLS_ROOT="${CLAUDETOOLS_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)}"
_logerr() { bash "$CLAUDETOOLS_ROOT/.claude/scripts/log-skill-error.sh" "1password" "$@" >/dev/null 2>&1 || true; }
VAULT=""
ITEM=""
OUTPUT=".env"
@@ -35,6 +39,7 @@ done
# Check op is available
if ! command -v op &>/dev/null; then
echo "❌ 1Password CLI (op) not found. Install: https://developer.1password.com/docs/cli/get-started/"
_logerr "op CLI not found on PATH"
exit 1
fi

View File

@@ -23,6 +23,10 @@
set -euo pipefail
# Functional-error logger (skill name "1password"); 4 levels up to the ClaudeTools repo.
CLAUDETOOLS_ROOT="${CLAUDETOOLS_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)}"
_logerr() { bash "$CLAUDETOOLS_ROOT/.claude/scripts/log-skill-error.sh" "1password" "$@" >/dev/null 2>&1 || true; }
VAULT="Dev"
ITEM=""
UPDATE=false
@@ -88,14 +92,16 @@ ALL_FIELDS=("${OP_FIELDS[@]+"${OP_FIELDS[@]}"}" "${SECRET_VALUES[@]+"${SECRET_VA
echo "Saving to 1Password..."
if $UPDATE; then
op item edit "$ITEM" --vault "$VAULT" "${ALL_FIELDS[@]}"
op item edit "$ITEM" --vault "$VAULT" "${ALL_FIELDS[@]}" \
|| { rc=$?; _logerr "op item edit failed (update MCP creds)" --context "item=$ITEM vault=$VAULT rc=$rc"; exit $rc; }
echo ""
echo "✅ Updated '$ITEM' in vault '$VAULT'"
else
# Try create, fall back to update if already exists
if op item get "$ITEM" --vault "$VAULT" &>/dev/null 2>&1; then
echo " Item already exists — updating instead..."
op item edit "$ITEM" --vault "$VAULT" "${ALL_FIELDS[@]}"
op item edit "$ITEM" --vault "$VAULT" "${ALL_FIELDS[@]}" \
|| { rc=$?; _logerr "op item edit failed (update MCP creds)" --context "item=$ITEM vault=$VAULT rc=$rc"; exit $rc; }
echo ""
echo "✅ Updated '$ITEM' in vault '$VAULT'"
else
@@ -103,7 +109,8 @@ else
--category API_CREDENTIAL \
--title "$ITEM" \
--vault "$VAULT" \
"${ALL_FIELDS[@]}"
"${ALL_FIELDS[@]}" \
|| { rc=$?; _logerr "op item create failed (MCP creds)" --context "item=$ITEM vault=$VAULT rc=$rc"; exit $rc; }
echo ""
echo "✅ Created '$ITEM' in vault '$VAULT'"
fi

View File

@@ -9,6 +9,10 @@
set -euo pipefail
# Functional-error logger (skill name "1password"); 4 levels up to the ClaudeTools repo.
CLAUDETOOLS_ROOT="${CLAUDETOOLS_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)}"
_logerr() { bash "$CLAUDETOOLS_ROOT/.claude/scripts/log-skill-error.sh" "1password" "$@" >/dev/null 2>&1 || true; }
TITLE=""
FIELD="credential"
VALUE=""
@@ -67,7 +71,8 @@ VAULT_FLAG=""
if $UPDATE; then
echo "Updating '${FIELD}' in '${TITLE}'..."
op item edit "$TITLE" $VAULT_FLAG "${FIELD}[password]=${VALUE}"
op item edit "$TITLE" $VAULT_FLAG "${FIELD}[password]=${VALUE}" \
|| { rc=$?; _logerr "op item edit failed (update secret)" --context "item=$TITLE field=$FIELD vault=${VAULT:-default} rc=$rc"; exit $rc; }
echo "✅ Updated '${FIELD}' in '${TITLE}'"
else
echo "Creating '${TITLE}' in 1Password..."
@@ -76,7 +81,8 @@ else
--title "$TITLE" \
$VAULT_FLAG \
"${FIELD}[password]=${VALUE}" \
--format=json)
--format=json) \
|| { rc=$?; _logerr "op item create failed" --context "item=$TITLE category=$CATEGORY vault=${VAULT:-default} rc=$rc"; exit $rc; }
ITEM_ID=$(echo "$RESULT" | jq -r '.id')
VAULT_NAME=$(echo "$RESULT" | jq -r '.vault.name')

View File

@@ -104,6 +104,8 @@ MODE="${1:-}"; shift 2>/dev/null || true
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
PF="$TMP/prompt.txt"; OUT="$TMP/out.txt"; ERR="$TMP/err.txt"
REPO_ROOT="${CLAUDETOOLS_ROOT:-$(cd "$SCRIPT_DIR/../../../.." 2>/dev/null && pwd)}"
# Functional-error logger (skill name "agy"); soft-fails, never breaks the caller.
_logerr() { bash "$REPO_ROOT/.claude/scripts/log-skill-error.sh" "agy" "$@" >/dev/null 2>&1 || true; }
# gtimeout on macOS (brew coreutils), timeout elsewhere.
TIMEOUT_CMD="timeout"
@@ -191,6 +193,7 @@ emit_or_fail() { # print .response, or retry once on a transient empty turn, el
# Auth failures won't be fixed by a retry — report immediately.
if auth_failed; then
echo "[$SELF] Gemini auth error — run 'gemini' interactively and choose 'Login with Google', then retry." >&2
_logerr "gemini auth/login failure" --context "mode=$MODE"
exit 1
fi
# Gemini occasionally returns an empty turn (or absorbs a 429 backoff into the
@@ -202,11 +205,13 @@ emit_or_fail() { # print .response, or retry once on a transient empty turn, el
if [ -n "$txt" ]; then printf '%s\n' "$txt"; return 0; fi
if auth_failed; then
echo "[$SELF] Gemini auth error — run 'gemini' interactively and choose 'Login with Google', then retry." >&2
_logerr "gemini auth/login failure (after retry)" --context "mode=$MODE"
exit 1
fi
fi
echo "[$SELF] no response from gemini. stderr tail:" >&2
tail -3 "$ERR" >&2 2>/dev/null || true
_logerr "gemini returned no response (empty after retry)" --context "mode=$MODE err=$(tail -1 "$ERR" 2>/dev/null | tr -d '\n' | cut -c1-80)"
exit 1
}

View File

@@ -32,12 +32,32 @@ from __future__ import annotations
import argparse
import json
import os
import subprocess
import sys
from typing import Optional
from b2_client import B2Client, B2Error, RATE_PER_GB_USD, BYTES_PER_GB, BYTES_PER_GIB
def _log_skill_error(skill, msg, context=""):
"""Soft-fail: append a functional-error entry to errorlog.md (never throws)."""
try:
root = os.environ.get("CLAUDETOOLS_ROOT") or os.path.abspath(
os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")
)
h = os.path.join(root, ".claude", "scripts", "log-skill-error.sh")
if not os.path.exists(h):
return
a = ["bash", h, skill, msg]
if context:
a += ["--context", context]
subprocess.run(a, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
timeout=10)
except Exception:
pass
def _emit(obj, as_json: bool, table_fn=None) -> None:
if as_json or table_fn is None:
print(json.dumps(obj, indent=2, default=str))
@@ -757,6 +777,10 @@ def main(argv=None) -> int:
return rc if isinstance(rc, int) else 0
except B2Error as exc:
print(f"[ERROR] {exc}", file=sys.stderr)
_log_skill_error("b2", f"{exc}",
context=f"cmd={getattr(args, 'command', '?')}"
+ (f" http={exc.status}" if exc.status else "")
+ (f" code={exc.code}" if exc.code else ""))
return 1
except KeyboardInterrupt:
return 130

View File

@@ -37,11 +37,31 @@ from __future__ import annotations
import argparse
import dataclasses
import json
import os
import subprocess
import sys
from gz_client import GravityZoneClient, GravityZoneError, GZEndpointSummary
def _log_skill_error(skill, msg, context=""):
"""Soft-fail: append a functional-error entry to errorlog.md (never throws)."""
try:
root = os.environ.get("CLAUDETOOLS_ROOT") or os.path.abspath(
os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")
)
h = os.path.join(root, ".claude", "scripts", "log-skill-error.sh")
if not os.path.exists(h):
return
a = ["bash", h, skill, msg]
if context:
a += ["--context", context]
subprocess.run(a, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
timeout=10)
except Exception:
pass
def _emit(obj, as_json: bool, table_fn=None) -> None:
if as_json or table_fn is None:
print(json.dumps(obj, indent=2, default=_json_default))
@@ -554,6 +574,8 @@ def main(argv=None) -> int:
return rc if isinstance(rc, int) else 0
except GravityZoneError as exc:
print(f"[ERROR] {exc}", file=sys.stderr)
_log_skill_error("bitdefender", f"{exc}",
context=f"cmd={getattr(args, 'command', '?')}")
return 1
except KeyboardInterrupt:
return 130

View File

@@ -21,7 +21,25 @@ Usage:
coord.py lock release <id>
coord.py lock list [--project KEY]
"""
import sys, os, json, argparse, urllib.request, urllib.error, urllib.parse
import sys, os, json, argparse, subprocess, urllib.request, urllib.error, urllib.parse
def _log_skill_error(skill, msg, context=""):
"""Soft-fail: append a functional-error entry to errorlog.md (never throws)."""
try:
root = os.environ.get("CLAUDETOOLS_ROOT") or os.path.abspath(
os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")
)
h = os.path.join(root, ".claude", "scripts", "log-skill-error.sh")
if not os.path.exists(h):
return
a = ["bash", h, skill, msg]
if context:
a += ["--context", context]
subprocess.run(a, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
timeout=10)
except Exception:
pass
def find_identity():
@@ -73,6 +91,10 @@ def call(method, path, body=None, query=None):
def die(st, resp, ok=(200, 201)):
if st not in ok:
print(f"[coord] ERROR HTTP {st}: {json.dumps(resp)[:500]}", file=sys.stderr)
_log_skill_error(
"coord", f"coord API call failed (HTTP {st})",
context=f"http={st} cmd={' '.join(sys.argv[1:3])} resp={json.dumps(resp)[:80]}",
)
sys.exit(1)

View File

@@ -83,6 +83,8 @@ WORK="$TMP/work"; mkdir -p "$WORK"
PF="$TMP/prompt.txt"; OUT="$TMP/out.json"
RUN_CWD="$WORK" # grok's working dir; the 'review' mode overrides to the repo so read_file can reach repo files
REPO_ROOT="${CLAUDETOOLS_ROOT:-$(cd "$SCRIPT_DIR/../../../.." 2>/dev/null && pwd)}"
# Functional-error logger (skill name "grok"); soft-fails, never breaks the caller.
_logerr() { bash "$REPO_ROOT/.claude/scripts/log-skill-error.sh" "grok" "$@" >/dev/null 2>&1 || true; }
# run grok headless. $1=timeout secs; rest=extra flags. Reads $PF -> $OUT.
# Never fails the script on grok's exit code (Cancelled is expected; we read artifacts).
@@ -170,7 +172,7 @@ case "$MODE" in
run_grok 180 --disable-web-search --max-turns 3
txt="$(jfield text)"
if [ -n "$txt" ]; then printf '%s\n' "$txt"; else
echo "[$SELF] no text (stopReason=$(jfield stopReason)); raw: $OUT" >&2; exit 1; fi
echo "[$SELF] no text (stopReason=$(jfield stopReason)); raw: $OUT" >&2; _logerr "grok returned no text" --context "mode=$MODE stopReason=$(jfield stopReason)"; exit 1; fi
;;
image)
[ -z "${1:-}" ] && { echo "usage: $SELF image \"<prompt>\" [out.png]" >&2; exit 2; }
@@ -180,7 +182,7 @@ case "$MODE" in
sid="$(jfield sessionId)"; art="$(find_artifact "$sid" images)"
if [ -n "$art" ] && [ -f "$art" ]; then cp -f "$art" "$out"
echo "[$SELF] image OK -> $out (session $sid)"
else echo "[$SELF] no image artifact (session=$sid, stopReason=$(jfield stopReason))" >&2; exit 1; fi
else echo "[$SELF] no image artifact (session=$sid, stopReason=$(jfield stopReason))" >&2; _logerr "grok produced no image artifact" --context "session=$sid stopReason=$(jfield stopReason)"; exit 1; fi
;;
video)
[ -z "${1:-}" ] || [ -z "${2:-}" ] && { echo "usage: $SELF video \"<prompt>\" <input-image> [out.mp4]" >&2; exit 2; }
@@ -192,7 +194,7 @@ case "$MODE" in
sid="$(jfield sessionId)"; art="$(find_artifact "$sid" videos)"
if [ -n "$art" ] && [ -f "$art" ]; then cp -f "$art" "$out"
echo "[$SELF] video OK -> $out (session $sid)"
else echo "[$SELF] no video artifact (session=$sid, stopReason=$(jfield stopReason))" >&2; exit 1; fi
else echo "[$SELF] no video artifact (session=$sid, stopReason=$(jfield stopReason))" >&2; _logerr "grok produced no video artifact" --context "session=$sid stopReason=$(jfield stopReason)"; exit 1; fi
;;
xsearch)
[ -z "${1:-}" ] && { echo "usage: $SELF xsearch \"<query>\"" >&2; exit 2; }
@@ -200,7 +202,7 @@ case "$MODE" in
run_grok 150 --max-turns 6
txt="$(jfield text)"
if [ -n "$txt" ]; then printf '%s\n' "$txt"; else
echo "[$SELF] no result (stopReason=$(jfield stopReason))" >&2; exit 1; fi
echo "[$SELF] no result (stopReason=$(jfield stopReason))" >&2; _logerr "grok xsearch returned no result" --context "mode=xsearch stopReason=$(jfield stopReason)"; exit 1; fi
;;
review|file)
[ -z "${1:-}" ] && { echo "usage: $SELF review <file-path> [instructions]" >&2; exit 2; }
@@ -233,7 +235,7 @@ case "$MODE" in
fi
fi
if [ -n "$txt" ]; then printf '%s\n' "$txt"; else
echo "[$SELF] no result (session=$(jfield sessionId), stopReason=$(jfield stopReason))" >&2; exit 1; fi
echo "[$SELF] no result (session=$(jfield sessionId), stopReason=$(jfield stopReason))" >&2; _logerr "grok review returned no result" --context "mode=$MODE session=$(jfield sessionId) stopReason=$(jfield stopReason)"; exit 1; fi
;;
review-files)
# review-files [-i "instructions"] <file> [file ...]
@@ -283,7 +285,7 @@ case "$MODE" in
fi
fi
if [ -n "$txt" ]; then printf '%s\n' "$txt"; else
echo "[$SELF] no result (session=$(jfield sessionId), stopReason=$(jfield stopReason))" >&2; exit 1; fi
echo "[$SELF] no result (session=$(jfield sessionId), stopReason=$(jfield stopReason))" >&2; _logerr "grok review returned no result" --context "mode=$MODE session=$(jfield sessionId) stopReason=$(jfield stopReason)"; exit 1; fi
;;
review-diff)
# review-diff [-C <repo-dir>] [-i "instructions"] <gitref> [-- <pathspec...>]
@@ -328,7 +330,7 @@ case "$MODE" in
fi
fi
if [ -n "$txt" ]; then printf '%s\n' "$txt"; else
echo "[$SELF] no result (session=$(jfield sessionId), stopReason=$(jfield stopReason))" >&2; exit 1; fi
echo "[$SELF] no result (session=$(jfield sessionId), stopReason=$(jfield stopReason))" >&2; _logerr "grok review returned no result" --context "mode=$MODE session=$(jfield sessionId) stopReason=$(jfield stopReason)"; exit 1; fi
;;
raw)
"$GROK" "$@"

View File

@@ -37,11 +37,31 @@ from __future__ import annotations
import argparse
import json
import os
import subprocess
import sys
from mp_client import MailprotectorClient, MailprotectorError, VALID_SCOPES
def _log_skill_error(skill, msg, context=""):
"""Soft-fail: append a functional-error entry to errorlog.md (never throws)."""
try:
root = os.environ.get("CLAUDETOOLS_ROOT") or os.path.abspath(
os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")
)
h = os.path.join(root, ".claude", "scripts", "log-skill-error.sh")
if not os.path.exists(h):
return
a = ["bash", h, skill, msg]
if context:
a += ["--context", context]
subprocess.run(a, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
timeout=10)
except Exception:
pass
def _emit(obj) -> None:
print(json.dumps(obj, indent=2, ensure_ascii=False, default=str))
@@ -314,6 +334,8 @@ def main(argv=None) -> int:
p.error(f"unknown command {args.cmd}")
except MailprotectorError as exc:
print(f"[ERROR] {exc}", file=sys.stderr)
_log_skill_error("mailprotector", f"{exc}",
context=f"cmd={getattr(args, 'cmd', '?')}")
return 1
return 0

View File

@@ -20,6 +20,7 @@ set -euo pipefail
TENANT_ADMIN_APPID="709e6eed-0711-4875-9c44-2d3518c47063"
CONSENT_BASE="https://login.microsoftonline.com"
CONSENT_REDIRECT="https://azcomputerguru.com"
__ROOT="${CLAUDETOOLS_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)}"
# ── Locate the reused remediation-tool scripts ────────────────────────────────
# Prefer the applied global copy (stable path on every fleet machine); fall back
@@ -43,6 +44,7 @@ RT="$(find_rtool)" || {
echo "[ERROR] remediation-tool scripts not found." >&2
echo " Expected: \$HOME/.claude/skills/remediation-tool/scripts/onboard-tenant.sh" >&2
echo " Run a repo sync, or check identity.json.claudetools_root." >&2
bash "$__ROOT/.claude/scripts/log-skill-error.sh" "onboard365" "onboard365: remediation-tool scripts not found (onboard-tenant.sh missing on this machine)" >/dev/null 2>&1 || true
exit 3
}

View File

@@ -34,11 +34,31 @@ from __future__ import annotations
import argparse
import json
import os
import subprocess
import sys
from ns_client import NetSapiensClient, PacketDialError
def _log_skill_error(skill, msg, context=""):
"""Soft-fail: append a functional-error entry to errorlog.md (never throws)."""
try:
root = os.environ.get("CLAUDETOOLS_ROOT") or os.path.abspath(
os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")
)
h = os.path.join(root, ".claude", "scripts", "log-skill-error.sh")
if not os.path.exists(h):
return
a = ["bash", h, skill, msg]
if context:
a += ["--context", context]
subprocess.run(a, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
timeout=10)
except Exception:
pass
def _emit(obj) -> None:
print(json.dumps(obj, indent=2, ensure_ascii=False, default=str))
@@ -161,6 +181,8 @@ def main(argv=None) -> int:
p.error(f"unknown command {args.cmd}")
except PacketDialError as exc:
print(f"[ERROR] {exc}", file=sys.stderr)
_log_skill_error("packetdial", f"{exc}",
context=f"cmd={getattr(args, 'cmd', '?')}")
return 1
return 0

View File

@@ -86,8 +86,10 @@ process_one() {
case "$rc" in
201) echo "ASSIGNED (Exchange Admin -> Exchange Operator SP)" ;;
400) if echo "$body" | grep -qiE 'conflicting object|already (exist|present)'; then echo "OK (already assigned)"
else echo "ERROR (HTTP 400: $(echo "$body" | jqr '.error.message // .' | head -c 120))"; fi ;;
*) echo "ERROR (HTTP $rc: $(echo "$body" | jqr '.error.message // .' | head -c 120))" ;;
else echo "ERROR (HTTP 400: $(echo "$body" | jqr '.error.message // .' | head -c 120))"
bash "$REPO_ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "assign-exchange-role: role assignment POST failed" --context "tenant=$tgt http=400 msg=$(echo "$body" | jqr '.error.message // .' | head -c 80)" >/dev/null 2>&1 || true; fi ;;
*) echo "ERROR (HTTP $rc: $(echo "$body" | jqr '.error.message // .' | head -c 120))"
bash "$REPO_ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "assign-exchange-role: role assignment POST failed" --context "tenant=$tgt http=$rc msg=$(echo "$body" | jqr '.error.message // .' | head -c 80)" >/dev/null 2>&1 || true ;;
esac
}
@@ -95,7 +97,7 @@ echo "=== assign-exchange-role [mode=$MODE] ==="
echo "Role: Exchange Administrator ($EXCH_ADMIN_TEMPLATE) -> SP: Exchange Operator ($EXCHANGE_OP_APPID)"
echo "------------------------------------------------------------------------"
if [ "$TARGET" = "--all" ]; then
[ -f "$TENANTS_MD" ] || { echo "[ERROR] tenants.md not found: $TENANTS_MD" >&2; exit 66; }
[ -f "$TENANTS_MD" ] || { echo "[ERROR] tenants.md not found: $TENANTS_MD" >&2; bash "$REPO_ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "assign-exchange-role: --all run but references/tenants.md not found" --context "path=$TENANTS_MD" >/dev/null 2>&1 || true; exit 66; }
# extract tenant GUIDs from the markdown table (column 3)
grep -oE '[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}' "$TENANTS_MD" \
| sort -u | while read -r tid; do process_one "$tid"; done

View File

@@ -239,6 +239,7 @@ case "$AUTH_OVERRIDE" in
if [[ -z "$CERT_X5T" || -z "$CERT_KEY_B64" ]]; then
echo "ERROR: REMEDIATION_AUTH=cert but cert fields missing in vault ($VAULT_PATH)" >&2
echo " Required fields under credentials: cert_thumbprint_b64url, cert_private_key_pem_b64" >&2
bash "$CLAUDETOOLS_ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "get-token: cert auth forced but cert fields missing in vault" --context "tier=$TIER vault=$VAULT_PATH" >/dev/null 2>&1 || true
exit 4
fi
AUTH_METHOD="cert"
@@ -251,6 +252,7 @@ case "$AUTH_OVERRIDE" in
if [[ -z "$CLIENT_SECRET" ]]; then
echo "ERROR: REMEDIATION_AUTH=secret but client_secret missing in vault ($VAULT_PATH)" >&2
echo " Check field: credentials.client_secret (or credentials.credential for older entries)" >&2
bash "$CLAUDETOOLS_ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "get-token: secret auth forced but client_secret missing in vault" --context "tier=$TIER vault=$VAULT_PATH" >/dev/null 2>&1 || true
exit 4
fi
AUTH_METHOD="secret"
@@ -269,6 +271,7 @@ case "$AUTH_OVERRIDE" in
echo "ERROR: no usable credential found in $VAULT_PATH" >&2
echo " Need either credentials.cert_thumbprint_b64url + credentials.cert_private_key_pem_b64," >&2
echo " or credentials.client_secret (or legacy credentials.credential)." >&2
bash "$CLAUDETOOLS_ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "get-token: no usable credential (cert or client_secret) in vault" --context "tier=$TIER vault=$VAULT_PATH" >/dev/null 2>&1 || true
exit 4
fi
AUTH_METHOD="secret"
@@ -336,6 +339,7 @@ PY
if [[ $ASSERT_RC -ne 0 || -z "$CLIENT_ASSERTION" ]]; then
echo "ERROR: failed to build client_assertion JWT" >&2
[[ -n "$CLIENT_ASSERTION" ]] && echo "$CLIENT_ASSERTION" >&2
bash "$CLAUDETOOLS_ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "get-token: failed to build client_assertion JWT (cert auth)" --context "tier=$TIER rc=$ASSERT_RC" >/dev/null 2>&1 || true
exit 4
fi
@@ -371,11 +375,13 @@ if [[ -z "$TOKEN" ]]; then
echo " After the admin accepts, run onboard-tenant.sh to assign required directory roles:" >&2
SCRIPT_DIR_ERR="$(dirname "${BASH_SOURCE[0]}")"
echo " bash ${SCRIPT_DIR_ERR}/onboard-tenant.sh ${TARGET}" >&2
bash "$CLAUDETOOLS_ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "get-token: AADSTS7000229 — app not consented in tenant" --context "tenant=$TENANT_ID tier=$TIER auth=$AUTH_METHOD" >/dev/null 2>&1 || true
exit 5
fi
echo "ERROR: token request failed (tenant=$TENANT_ID tier=$TIER auth=$AUTH_METHOD)" >&2
echo "$RESP" >&2
bash "$CLAUDETOOLS_ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "get-token: token request failed (no access_token)" --context "tenant=$TENANT_ID tier=$TIER auth=$AUTH_METHOD err=${ERROR_CODE:-none}" >/dev/null 2>&1 || true
exit 5
fi

View File

@@ -23,6 +23,7 @@
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
__ROOT="${CLAUDETOOLS_ROOT:-$(cd "$SCRIPT_DIR/../../../.." && pwd)}"
TARGET="${1:?Usage: onboard-tenant.sh <domain-or-tenant-id> [--dry-run]}"
DRY_RUN=false
@@ -182,6 +183,7 @@ create_sp_if_missing() {
return 0
fi
echo " [ERROR] Failed to create SP for $app_name: $(echo "$resp" | jq -r '.error.message // empty')" >&2
bash "$__ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "onboard-tenant: failed to create service principal" --context "app=$app_name appId=$app_id msg=$(echo "$resp" | jq -r '.error.message // empty' | head -c 80)" >/dev/null 2>&1 || true
return 1
fi
@@ -239,6 +241,7 @@ grant_app_role() {
return 0
fi
echo " [ERROR] grant_app_role failed for $role_id: $(echo "$resp" | jq -r '.error.message // "unknown"')" >&2
bash "$__ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "onboard-tenant: grant_app_role appRoleAssignment failed" --context "role=$role_id msg=$(echo "$resp" | jq -r '.error.message // "unknown"' | head -c 80)" >/dev/null 2>&1 || true
return 1
fi
}
@@ -380,6 +383,7 @@ assign_role() {
fi
echo " [ERROR] Failed to assign $role_name" >&2
echo " Response: $resp" >&2
bash "$__ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "onboard-tenant: failed to assign directory role" --context "role=$role_name sp=$sp_oid msg=$(echo "$resp" | jq -r '.error.message // empty' | head -c 80)" >/dev/null 2>&1 || true
return 1
fi
echo " [OK] $role_name assigned (assignment id=$assigned_id)"
@@ -390,6 +394,7 @@ echo "[INFO] Resolving tenant: $TARGET"
TENANT_ID=$("$SCRIPT_DIR/resolve-tenant.sh" "$TARGET")
if [[ -z "$TENANT_ID" ]]; then
echo "[ERROR] Could not resolve tenant ID for: $TARGET" >&2
bash "$__ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "onboard-tenant: could not resolve tenant ID" --context "target=$TARGET" >/dev/null 2>&1 || true
exit 1
fi
@@ -412,6 +417,7 @@ if [[ $GET_TOKEN_EXIT -ne 0 ]]; then
fi
echo "[ERROR] Failed to acquire Tenant Admin token (exit $GET_TOKEN_EXIT)" >&2
echo "$TOKEN_ERR" >&2
bash "$__ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "onboard-tenant: failed to acquire Tenant Admin token" --context "tenant=$TENANT_ID exit=$GET_TOKEN_EXIT" >/dev/null 2>&1 || true
exit 5
fi
TENANT_ADMIN_TOKEN="$TENANT_ADMIN_TOKEN_OUT"
@@ -440,6 +446,7 @@ DEFENDER_SP_OID=$(get_sp_oid "$TENANT_ADMIN_TOKEN" "$DEFENDER_APP_ID")
if [[ -z "$GRAPH_SP_OID" ]]; then
echo "[ERROR] Microsoft Graph SP missing — cannot grant app permissions" >&2
bash "$__ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "onboard-tenant: Microsoft Graph SP missing in tenant — cannot grant app permissions" --context "tenant=$TENANT_ID" >/dev/null 2>&1 || true
exit 1
fi

View File

@@ -20,6 +20,8 @@ ROLE_MGMT_PERMISSION_ID="9e3f62cf-ca93-4989-b6ce-bf83c28f9fe8"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CLAUDETOOLS_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
IDENTITY_FILE="$CLAUDETOOLS_ROOT/.claude/identity.json"
# Repo root for the functional-error logger (4 levels up from this scripts dir).
__ELOG_ROOT="${CLAUDETOOLS_ROOT_ENV:-$(cd "$SCRIPT_DIR/../../../.." && pwd)}"
VAULT_ROOT="${VAULT_PATH:-}"
if [[ -z "$VAULT_ROOT" && -f "$IDENTITY_FILE" ]]; then
@@ -39,7 +41,7 @@ CLIENT_SECRET=$(bash "$VAULT_ROOT/scripts/vault.sh" get-field "$MANAGEMENT_VAULT
if [[ -z "$CLIENT_SECRET" ]]; then
CLIENT_SECRET=$(bash "$VAULT_ROOT/scripts/vault.sh" get-field "$MANAGEMENT_VAULT_PATH" credentials.credential 2>/dev/null | tr -d '\r\n' || true)
fi
[[ -z "$CLIENT_SECRET" ]] && { echo "[ERROR] Could not read secret from $MANAGEMENT_VAULT_PATH" >&2; exit 4; }
[[ -z "$CLIENT_SECRET" ]] && { echo "[ERROR] Could not read secret from $MANAGEMENT_VAULT_PATH" >&2; bash "$__ELOG_ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "patch-tenant-admin-manifest: could not read Management app secret from vault" --context "vault=$MANAGEMENT_VAULT_PATH" >/dev/null 2>&1 || true; exit 4; }
echo "[OK] Management app secret retrieved"
# ── Step 2: Get Management app token (home tenant) ───────────────────────────
@@ -55,6 +57,7 @@ MGMT_TOKEN=$(echo "$TOKEN_RESP" | jq -r '.access_token // empty')
if [[ -z "$MGMT_TOKEN" ]]; then
echo "[ERROR] Failed to acquire Management app token" >&2
echo "$TOKEN_RESP" >&2
bash "$__ELOG_ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "patch-tenant-admin-manifest: failed to acquire Management app token (home tenant)" --context "err=$(echo "$TOKEN_RESP" | jq -r '.error // empty' 2>/dev/null)" >/dev/null 2>&1 || true
exit 5
fi
echo "[OK] Management app token acquired"
@@ -73,6 +76,7 @@ APP_DISPLAY=$(echo "$APP_RESP" | jq -r '.value[0].displayName // empty')
if [[ -z "$APP_OBJ_ID" ]]; then
echo "[ERROR] Tenant Admin application not found (appId=$TENANT_ADMIN_APP_ID)" >&2
echo "Response: $APP_RESP" >&2
bash "$__ELOG_ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "patch-tenant-admin-manifest: Tenant Admin application registration not found" --context "appId=$TENANT_ADMIN_APP_ID" >/dev/null 2>&1 || true
exit 6
fi
echo "[OK] Found app: $APP_DISPLAY (objectId=$APP_OBJ_ID)"
@@ -108,6 +112,7 @@ else
echo "[OK] App manifest patched (HTTP 204)"
else
echo "[ERROR] PATCH returned HTTP $PATCH_RESP" >&2
bash "$__ELOG_ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "patch-tenant-admin-manifest: app manifest PATCH failed" --context "appObjId=$APP_OBJ_ID http=$PATCH_RESP" >/dev/null 2>&1 || true
exit 7
fi
fi
@@ -121,7 +126,7 @@ TA_SP_RESP=$(curl -s --max-time 15 \
--data-urlencode "\$select=id,displayName" \
"https://graph.microsoft.com/v1.0/servicePrincipals")
TA_SP_OID=$(echo "$TA_SP_RESP" | jq -r '.value[0].id // empty')
[[ -z "$TA_SP_OID" ]] && { echo "[ERROR] Tenant Admin SP not found in home tenant" >&2; exit 8; }
[[ -z "$TA_SP_OID" ]] && { echo "[ERROR] Tenant Admin SP not found in home tenant" >&2; bash "$__ELOG_ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "patch-tenant-admin-manifest: Tenant Admin SP not found in home tenant" --context "appId=$TENANT_ADMIN_APP_ID" >/dev/null 2>&1 || true; exit 8; }
echo "[OK] Tenant Admin SP: $TA_SP_OID"
echo "[INFO] Locating Microsoft Graph SP in home tenant..."
@@ -132,7 +137,7 @@ GRAPH_SP_RESP=$(curl -s --max-time 15 \
--data-urlencode "\$select=id" \
"https://graph.microsoft.com/v1.0/servicePrincipals")
GRAPH_SP_OID=$(echo "$GRAPH_SP_RESP" | jq -r '.value[0].id // empty')
[[ -z "$GRAPH_SP_OID" ]] && { echo "[ERROR] Microsoft Graph SP not found in home tenant" >&2; exit 8; }
[[ -z "$GRAPH_SP_OID" ]] && { echo "[ERROR] Microsoft Graph SP not found in home tenant" >&2; bash "$__ELOG_ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "patch-tenant-admin-manifest: Microsoft Graph SP not found in home tenant" >/dev/null 2>&1 || true; exit 8; }
echo "[OK] Microsoft Graph SP: $GRAPH_SP_OID"
# ── Step 6: Check if appRoleAssignment already granted ────────────────────────
@@ -165,6 +170,7 @@ else
if [[ -z "$GRANT_ID" ]]; then
echo "[ERROR] Failed to grant appRoleAssignment" >&2
echo "$GRANT_RESP" >&2
bash "$__ELOG_ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "patch-tenant-admin-manifest: failed to grant RoleManagement.ReadWrite.Directory appRoleAssignment" --context "msg=$(echo "$GRANT_RESP" | jq -r '.error.message // empty' 2>/dev/null | head -c 80)" >/dev/null 2>&1 || true
exit 9
fi
echo "[OK] appRoleAssignment granted (id=$GRANT_ID)"

View File

@@ -23,6 +23,7 @@
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
__ROOT="${CLAUDETOOLS_ROOT:-$(cd "$SCRIPT_DIR/../../../.." && pwd)}"
TENANT_INPUT="${1:?usage: reset-password.sh <tenant|domain> <upn> <new-password> [--force-change]}"
UPN="${2:?usage: reset-password.sh <tenant|domain> <upn> <new-password> [--force-change]}"
@@ -43,7 +44,7 @@ G="https://graph.microsoft.com/v1.0"
# --- resolve target user object id ---
UID_=$(curl -s "${GH[@]}" "$G/users/${UPN}?\$select=id" | tr -d '\000-\037' \
| python -c "import sys,json;print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
[[ -z "$UID_" ]] && { echo "[ERROR] user not found: $UPN" >&2; exit 1; }
[[ -z "$UID_" ]] && { echo "[ERROR] user not found: $UPN" >&2; bash "$__ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "reset-password: target user not found / Graph returned no id" --context "tenant=$TENANT_ID upn=$UPN" >/dev/null 2>&1 || true; exit 1; }
echo "[info] tenant=$TENANT_ID target=$UPN id=$UID_ force_change=$FORCE_CHANGE"
# --- build payload (single-quoted heredoc would block $NEWPW; use python to emit JSON safely) ---
@@ -61,6 +62,7 @@ fi
if [[ "$CODE" != "403" ]]; then
echo "[ERROR] unexpected HTTP $CODE on password PATCH" >&2
curl -s -X PATCH "${GH[@]}" "$G/users/$UID_" --data-binary "$PAYLOAD" | tr -d '\000-\037' >&2
bash "$__ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "reset-password: unexpected HTTP on password PATCH" --context "tenant=$TENANT_ID upn=$UPN http=$CODE" >/dev/null 2>&1 || true
exit 1
fi
@@ -69,7 +71,7 @@ echo "[info] 403 on direct reset (target likely holds an admin role) -> JIT elev
# --- resolve tenant-admin SP object id ---
SPID=$(curl -s "${GH[@]}" "$G/servicePrincipals(appId='$TENANT_ADMIN_APPID')?\$select=id" | tr -d '\000-\037' \
| python -c "import sys,json;print(json.load(sys.stdin).get('id',''))")
[[ -z "$SPID" ]] && { echo "[ERROR] could not resolve Tenant Admin service principal" >&2; exit 1; }
[[ -z "$SPID" ]] && { echo "[ERROR] could not resolve Tenant Admin service principal" >&2; bash "$__ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "reset-password: could not resolve Tenant Admin SP for JIT elevation" --context "tenant=$TENANT_ID" >/dev/null 2>&1 || true; exit 1; }
# --- does the SP already hold Privileged Authentication Administrator? ---
EXISTING=$(curl -s "${GH[@]}" "$G/roleManagement/directory/roleAssignments?\$filter=principalId+eq+'$SPID'+and+roleDefinitionId+eq+'$PAA_ROLE_ID'" \
@@ -82,7 +84,7 @@ else
ASSIGN_BODY=$(SPID="$SPID" RID="$PAA_ROLE_ID" python -c "import os,json;print(json.dumps({'principalId':os.environ['SPID'],'roleDefinitionId':os.environ['RID'],'directoryScopeId':'/'}))")
CREATED_ASSIGNMENT=$(curl -s -X POST "${GH[@]}" "$G/roleManagement/directory/roleAssignments" --data-binary "$ASSIGN_BODY" \
| tr -d '\000-\037' | python -c "import sys,json;d=json.load(sys.stdin);print(d.get('id',''))" 2>/dev/null || true)
[[ -z "$CREATED_ASSIGNMENT" ]] && { echo "[ERROR] failed to assign Privileged Authentication Administrator to SP" >&2; exit 1; }
[[ -z "$CREATED_ASSIGNMENT" ]] && { echo "[ERROR] failed to assign Privileged Authentication Administrator to SP" >&2; bash "$__ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "reset-password: failed to assign Privileged Authentication Administrator to SP (JIT elevation)" --context "tenant=$TENANT_ID sp=$SPID" >/dev/null 2>&1 || true; exit 1; }
echo "[info] assigned Privileged Authentication Administrator to SP (assignment $CREATED_ASSIGNMENT)"
fi
@@ -90,7 +92,7 @@ fi
cleanup() {
if [[ -n "$CREATED_ASSIGNMENT" ]]; then
DC=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE "${GH[@]}" "$G/roleManagement/directory/roleAssignments/$CREATED_ASSIGNMENT")
if [[ "$DC" == "204" ]]; then echo "[info] removed JIT role assignment (de-elevated)"; else echo "[WARNING] failed to remove JIT role assignment $CREATED_ASSIGNMENT (HTTP $DC) - REMOVE MANUALLY" >&2; fi
if [[ "$DC" == "204" ]]; then echo "[info] removed JIT role assignment (de-elevated)"; else echo "[WARNING] failed to remove JIT role assignment $CREATED_ASSIGNMENT (HTTP $DC) - REMOVE MANUALLY" >&2; bash "$__ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "reset-password: failed to remove JIT Privileged Auth Admin role - standing privilege left behind, REMOVE MANUALLY" --context "tenant=$TENANT_ID assignment=$CREATED_ASSIGNMENT http=$DC" >/dev/null 2>&1 || true; fi
fi
}
trap cleanup EXIT
@@ -108,4 +110,5 @@ done
echo "[ERROR] password reset still failing after elevation (last HTTP $CODE)" >&2
curl -s -X PATCH "${GH[@]}" "$G/users/$UID_" --data-binary "$PAYLOAD" | tr -d '\000-\037' >&2
bash "$__ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "reset-password: reset still failing after JIT elevation + retries" --context "tenant=$TENANT_ID upn=$UPN http=$CODE" >/dev/null 2>&1 || true
exit 1

View File

@@ -4,6 +4,8 @@
# Output (stdout): tenant GUID. Exit 0 on success, 1 on failure.
set -euo pipefail
__ROOT="${CLAUDETOOLS_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)}"
INPUT="${1:?usage: resolve-tenant.sh <domain|upn|tenant-id>}"
# If it looks like a GUID already, pass through.
@@ -31,6 +33,7 @@ TENANT_ID=$(echo "$RESP" | jq -r '.issuer // empty' | sed -E 's|^https://login\.
if [[ -z "$TENANT_ID" ]] || [[ ! "$TENANT_ID" =~ ^[0-9a-fA-F]{8}- ]]; then
echo "ERROR: could not resolve tenant for domain: $DOMAIN" >&2
echo "Response: $RESP" >&2
bash "$__ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "resolve-tenant: OpenID discovery did not return a tenant GUID" --context "domain=$DOMAIN" >/dev/null 2>&1 || true
exit 1
fi

View File

@@ -6,6 +6,7 @@
set -euo pipefail
SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")"
__ROOT="${CLAUDETOOLS_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)}"
TENANT_INPUT="${1:?usage: user-breach-check.sh <tenant-id|domain> <upn>}"
UPN="${2:?usage: user-breach-check.sh <tenant-id|domain> <upn>}"
@@ -28,6 +29,7 @@ UID_=$(jq -r '.id // empty' "$OUT/00_user.json")
if [[ -z "$UID_" ]]; then
echo "ERROR: user not found or Graph returned error" >&2
cat "$OUT/00_user.json" >&2
bash "$__ROOT/.claude/scripts/log-skill-error.sh" "remediation-tool" "user-breach-check: user not found or Graph returned error resolving user object" --context "tenant=$TENANT_ID upn=$UPN err=$(jq -r '.error.code // empty' "$OUT/00_user.json" 2>/dev/null)" >/dev/null 2>&1 || true
exit 1
fi
echo "[info] object id: $UID_"

View File

@@ -98,6 +98,23 @@ After creating the files:
- Commands: Tell them to use `/{name}` or `/{name} arguments`
3. Remind them to update CLAUDE.md's Commands & Skills table if they want it documented there
## Mandatory: functional error logging
**Every skill MUST report genuine functional errors to `errorlog.md`** via the canonical
helper, so failures can be linted and fed back into skill improvements (CLAUDE.md core rule).
Bake this into the skill from the start:
- In each skill **script's failure branches** (API/auth failure, unexpected response,
validation error, unexpected non-zero exit), call:
```bash
bash "$ROOT/.claude/scripts/log-skill-error.sh" "<skill-name>" "<brief error>" --context "op=... http=..."
```
It stamps date+machine, inserts in the standard `YYYY-MM-DD | MACHINE | skill | error`
format, and soft-fails (never breaks the caller). Python skills shell out to the same helper.
- In the **SKILL.md**, add a line under workflow/guidelines: "On a functional error, log it via
`log-skill-error.sh` before surfacing it."
- Do NOT log expected/handled conditions (no results, no unread, user-declined) — only real failures.
## Quality Checklist
Before finalizing, verify:
@@ -107,6 +124,7 @@ Before finalizing, verify:
- [ ] File is in the correct location (`.claude/skills/` or `.claude/commands/`)
- [ ] Name uses kebab-case and is concise
- [ ] For skills with auto-triggers: triggers are specific enough to avoid false positives
- [ ] **Functional errors are logged** via `log-skill-error.sh` in the script's failure branches
## Tips for Good Skills/Commands

View File

@@ -25,6 +25,12 @@
# Anything you put OUTSIDE those keys is committed in PLAINTEXT — never do that.
set -euo pipefail
# ── Functional-error logger ───────────────────────────────────────────────────
# errorlog.md + log-skill-error.sh live in the ClaudeTools repo (4 levels up from
# .claude/skills/vault/scripts/), NOT the SOPS vault repo (VAULT_DIR below).
CLAUDETOOLS_ROOT="${CLAUDETOOLS_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)}"
_logerr() { bash "$CLAUDETOOLS_ROOT/.claude/scripts/log-skill-error.sh" "vault" "$@" >/dev/null 2>&1 || true; }
# ── Resolve vault root (the #1 thing sessions get wrong) ──────────────────────
# Order: $VAULT_PATH override → the repo we're standing in (correct identity) →
# $HOME identity vault_path → $HOME identity claudetools_root → that repo's identity.
@@ -84,7 +90,7 @@ cmd_verify() {
local f; f=$(abspath "${1:?usage: verify <path>}")
[[ -f "$f" ]] || { echo "[ERROR] not found: $f" >&2; exit 1; }
if ! _is_encrypted "$f"; then echo "[FAIL] $1 — NO encrypted values found (plaintext?)"; exit 1; fi
if ! ( cd "$VAULT_DIR" && sops -d "$f" >/dev/null 2>&1 ); then echo "[FAIL] $1 — encrypted but does not decrypt (key mismatch?)"; exit 1; fi
if ! ( cd "$VAULT_DIR" && sops -d "$f" >/dev/null 2>&1 ); then echo "[FAIL] $1 — encrypted but does not decrypt (key mismatch?)"; _logerr "sops decrypt failed (key mismatch?)" --context "op=verify path=$1"; exit 1; fi
echo "[OK] $1 — encrypted and decrypts cleanly"
}
@@ -129,7 +135,7 @@ doc["notes"]=""
with open(f,"w",encoding="utf-8",newline="\n") as fh:
yaml.safe_dump(doc,fh,default_flow_style=False,sort_keys=False,allow_unicode=True)
PY
( cd "$VAULT_DIR" && sops --encrypt --in-place "$f" ) || { echo "[ERROR] sops encrypt failed; removing plaintext" >&2; rm -f "$f"; exit 1; }
( cd "$VAULT_DIR" && sops --encrypt --in-place "$f" ) || { echo "[ERROR] sops encrypt failed; removing plaintext" >&2; _logerr "sops encrypt failed (new entry)" --context "op=new path=$path"; rm -f "$f"; exit 1; }
cmd_verify "$path"
echo "[INFO] Created ${f#$VAULT_DIR/}. Publish with: bash .claude/scripts/sync.sh (Phase 6 commits+pushes the vault)"
}
@@ -142,7 +148,7 @@ cmd_set() {
local f; f=$(abspath "$path")
[[ -f "$f" ]] || { echo "[ERROR] not found: ${f#$VAULT_DIR/} (use 'new' to create)" >&2; exit 1; }
local tmp; tmp=$(mktemp)
( cd "$VAULT_DIR" && sops -d "$f" ) > "$tmp" 2>/dev/null || { echo "[ERROR] decrypt failed" >&2; rm -f "$tmp"; exit 1; }
( cd "$VAULT_DIR" && sops -d "$f" ) > "$tmp" 2>/dev/null || { echo "[ERROR] decrypt failed" >&2; _logerr "sops decrypt failed (set)" --context "op=set path=$path"; rm -f "$tmp"; exit 1; }
SETS="$(printf '%s\n' "${sets[@]}")" "$PY" - "$tmp" <<'PY'
import os,sys,yaml
f=sys.argv[1]
@@ -154,7 +160,7 @@ for kv in os.environ["SETS"].splitlines():
yaml.safe_dump(doc,open(f,"w",encoding="utf-8",newline="\n"),default_flow_style=False,sort_keys=False,allow_unicode=True)
PY
cp "$tmp" "$f"; rm -f "$tmp"
( cd "$VAULT_DIR" && sops --encrypt --in-place "$f" ) || { echo "[ERROR] re-encrypt failed" >&2; exit 1; }
( cd "$VAULT_DIR" && sops --encrypt --in-place "$f" ) || { echo "[ERROR] re-encrypt failed" >&2; _logerr "sops re-encrypt failed (set)" --context "op=set path=$path"; exit 1; }
cmd_verify "$path"
echo "[INFO] Updated ${f#$VAULT_DIR/}. Publish with: bash .claude/scripts/sync.sh"
}

View File

@@ -1,14 +1,37 @@
# Error Log
Brief records of task-execution errors across the fleet, used to improve skills and the
command harness. Append newest entries at the top. Keep each entry to 1-2 lines.
Brief records of preventable, pattern-worthy events across the fleet used to improve
skills, write better CLAUDE.md rules, and clean stale/misleading memory. The aim: never
pay tokens twice for the same avoidable mistake. Append newest at the top; keep entries to
1-2 lines. **Always write via the helper, never by hand:**
`bash .claude/scripts/log-skill-error.sh "<skill/context>" "<brief>" [--correction|--friction] [--context "k=v"]`
Format: `YYYY-MM-DD | MACHINE | command/skill | error (brief)`
Format: `YYYY-MM-DD | MACHINE | command/skill/context | [type] error (brief) [ctx: ...]`
Categories (the `[type]` tag): _(none)_ = skill/command execution failure ·
`[correction]` = user corrected an improper assumption I made ·
`[friction]` = preventable self-inflicted token-waste (harness/env/tool misuse; cite a
`ref=` in ctx when it repeats a documented gotcha — that flags a rule/memory to strengthen).
---
<!-- Append entries below this line -->
2026-06-15 | GURU-5070 | powershell/var-case | [friction] PowerShell vars are case-INSENSITIVE: $gUid silently overwrote $guid (GPO id), Set-ADObject hit a bad DN and left GPT.ini/AD versionNumber inconsistent until fixed. Never rely on case to distinguish PS variables
2026-06-15 | GURU-5070 | python/argv-limit | [friction] passed full /api/agents JSON (248 agents) as a python CLI arg -> 'Argument list too long' on Windows. Pipe large payloads via stdin, not argv
2026-06-15 | GURU-5070 | bash/env-persist | [friction] re-derived RMM token every call after $TOKEN/$RMM vanished between Bash tool calls - shell env does NOT persist across calls; must re-eval auth (or chain) in the same command
2026-06-15 | GURU-5070 | bash/tmp-path | [friction] wrote curl -o /tmp/x.json then jq read it back and failed (No such file) - Git-Bash vs Write/tool /tmp resolve differently. Pipe directly or use repo-relative paths. REPEAT of documented gotcha [ctx: ref=feedback_tmp_path_windows]
2026-06-15 | GURU-5070 | DMARC / DNS | [correction] assumed ACG's own INKY rua convention (reports-sg.inkydmarc.com) applied to a client domain; only use the INKY rua if THAT client is onboarded to INKY - otherwise plain p=none or a real mailbox
2026-06-15 | GURU-5070 | remediation-tool (sendMail) | [correction] assumed none of the consented apps could send mail and started granting Graph Mail.Send; the Exchange Operator app ALREADY had Graph Mail.Send - I was decoding the EXO-audience token, not a Graph-audience token. Mint a Graph token for the app before concluding a permission is missing
2026-06-15 | GURU-5070 | rmm-search | [correction] assumed the CLI search must replicate the UI Omnibox scoreMatch exactly; user wants a FLEXIBLE forgiving multi-field search optimized for first-try correctness, not UI parity
2026-06-15 | GURU-BEAST-ROG | /syncro (comment edit) | Syncro API does not expose a comment-edit or comment-delete endpoint — once posted, comments can only be modified via the GUI. Bot posted an internal resolution note with an unwanted "Performed by: ClaudeTools Discord Bot" line and could not remove it programmatically. Remediation needed: either suppress bot-attribution lines from internal notes by default, or add a GUI-edit step to the workflow when the note needs correction.
2026-06-14 | GURU-5070 | mailbox skill (Graph token) | FABB app `fabb3421` (Claude-MSP-Access / "Cloud MSP Access") token request returned AADSTS700016 — app/SP no longer present in azcomputerguru.com tenant (deleted; gotchas.md already marked it deprecated). Blocks /mailbox + the M365 contacts task. Verified the remediation suite (live, ACG tenant) carries NO Mail.Send/Mail.ReadWrite/Contacts scopes (investigator has Mail.Read only) — so a straight repoint can't restore mailbox-send/contacts. Pending Mike decision: stand up a single-tenant ACG-internal mailbox app vs. add scopes to a suite tier. [2026-06-15] Docs hardened — gotchas.md now marks fabb3421 DELETED with the Mail/Contacts-scope blast radius + flags the 3 legacy "old app only" tenants (Valleywide/Dataforth/Cascades) as now having NO working remediation app (migration URGENT); mailbox.md carries a BLOCKED/AADSTS700016 banner. DECISION 2026-06-15 (Mike): Mail.Send goes into the suite (Exchange Operator tier) since its real use is IR victim-notification during mailbox takeovers; add Mail.Send to the exchange-op manifest + consent, repoint mailbox.md to exchange-op. Implementation not yet executed (production app change, needs go).