Files
hermes-skills/ask-claude/SKILL.md

347 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: ask-claude
description: "Consult Claude Opus 4.8 on 10.0.0.28 via SSH print mode with session resumption. Multi-turn back-and-forth without tmux or polling."
version: 2.3.0
author: Hermes Agent
license: MIT
platforms: [linux]
metadata:
hermes:
tags: [claude, opus, consulting, planning, analysis, ssh, print-mode, usage-tracking]
related_skills: [claude-code, hermes-agent]
---
# Ask Claude — Print Mode Consulting via Claude Opus 4.8
Includes usage & context tracking: context % from JSON envelope (free), session/weekly % from OAuth usage API (best-effort, not per-turn). Alerts at 80% context, 80% session, 90% weekly.
Consult Claude Opus on 10.0.0.28 using headless print mode (`-p`) with session resumption. Each turn is a request/response — Claude's process exit IS the turn boundary. No tmux, no polling, no send-keys races.
## ⚡ Quick Reference
| Step | What | Command Pattern |
|------|------|----------------|
| 1. Write question | Temp file on 10.0.0.28 via mktemp | `ssh ... 'mktemp ... && cat > $QFILE << "EOF"\nquestion\nEOF'` |
| 2. First question | No `--resume`, `|| true` to survive errors | `timeout 280 claude -p "$(cat $QFILE)" --output-format json --model claude-opus-4-8 --max-turns 30 || true` |
| 3. Parse result | Extract `session_id`, `result`, `is_error`, detect `error_max_turns` | `python3 -c "import sys,json; ..."` |
| 4. Follow-up | WITH `--resume <sid>` | `claude -p --resume <sid> "$(cat $QFILE)" --output-format json --model claude-opus-4-8 --max-turns 30 || true` |
| 5. Done | Process exits. Session auto-expires in ~5h. | `rm $QFILE` (trap on EXIT) |
**Critical:** Always use `--output-format json`, `--model claude-opus-4-8`, and `--max-turns 30`. Pin the model explicitly — profile defaults drift silently. Use `|| true` after the claude command to survive non-zero exits (errors still produce parseable JSON). Use full path `/home/n8n/.local/bin/claude` on non-interactive SSH. Wrap with `timeout 280` to prevent runaway hangs.
## Decision Point: `--resume` or Fresh Session?
**Default rule: One Hermes session = one Claude session.** The first `ask-claude` call in a Hermes session starts a fresh Claude session. Every subsequent call within the same Hermes session resumes it. Claude accumulates context across turns, just like our conversation does.
```
Is there an active Claude session_id from a previous turn in THIS Hermes session?
├─ YES → Use --resume <session_id>
│ Always. Claude remembers everything from earlier in our conversation.
│ This is the default — same Hermes session, same Claude session.
└─ NO → Fresh session (no --resume flag)
Only when:
- This is the first ask-claude call in this Hermes session
- The user explicitly said "new topic" or "fresh session"
- The previous session expired (~5 hours since last turn)
```
**Session lifecycle:** Each `claude -p` invocation exits immediately after responding. The session data lives on disk. You don't "close" sessions — they auto-expire after ~5 hours of inactivity. No cleanup needed.
## When to Use
- Deep analysis, planning, or validation
- Second opinion on architecture, security, or design
- Research that benefits from SearXNG + Opus reasoning
- Multi-turn discussion where Claude may ask clarifying questions
## Local Hermes Peer (Alternative)
When Claude is unreachable, overkill, or the user wants a free local peer, use the `ask-hermes` skill (planned, not yet built — see `/home/n8n/workspace/dev/ask_hermes_plan.md`). It starts a persistent peer Hermes agent on a different profile via plain `hermes` (interactive REPL, no flags). Three commands: `start ask` / `ask hermes <instructions>` / `stop ask`. The peer is a full delegated worker with all tools — not just a Q&A box. See the plan for the full mechanism, prompt composition rules, and turn budget (max 20, soft limit).
**Build-attempt findings (2026-06-30):**
- `hermes -p general` works — `-p` flag exists despite not appearing in top-level `--help`.
- REPL needs `pty=true` to stay alive; without it, prompt_toolkit exits immediately ("Input is not a terminal").
- **Blocker: TUI output is unreadable via `process(poll)`.** The interactive REPL's ANSI redraw codes make the actual response text unparseable. Input works, output doesn't. This is the open problem — the plan's mechanism (plain `hermes`, interactive REPL, no flags) can't currently read peer responses cleanly. Possible paths: `--cli` flag for classic REPL, or headless `-z` one-shot with `--resume` for continuity.
## Prerequisites
- Claude Code on 10.0.0.28 at `/home/n8n/.local/bin/claude` (v2.1.195, native installer)
- Consulting workspace: `~/claude/hermes_support/` with CLAUDE.md
- SSH: `ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=accept-new -o ServerAliveInterval=30 n8n@10.0.0.28`
- Model: `claude-opus-4-8` — pass explicitly via `--model`, do NOT rely on profile default
- Use full path `/home/n8n/.local/bin/claude` on non-interactive SSH (bare `claude` only works in interactive shells that source `.bashrc`)
- Wrap with `timeout 280` to prevent runaway hangs
- Use `|| true` after claude invocation — `set -e` would kill the script on non-zero exit, but errors still produce parseable JSON
## Workflow
### Step 1: Confirm with User
Tell the user what question you'll send. Skip confirmation for clear, self-contained queries. Confirm when the question needs interpretation.
### Step 2: Send First Question (Fresh Session)
**CRITICAL: Never inline the question in the SSH command.** Shell metacharacters (`"`, `$`, `(`, `)`, backticks) will break. Always write to a temp file first. Use `mktemp` to avoid collisions between concurrent sessions.
```bash
# Step 2a: Write question to temp file on 10.0.0.28 (mktemp avoids collisions)
QFILE=$(ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=accept-new -o ServerAliveInterval=30 n8n@10.0.0.28 \
'f=$(mktemp /tmp/ask-claude-q-XXXXXX.txt) && cat > "$f" << "HERMES_EOF"
QUESTION TEXT HERE - any characters are safe: quotes "quotes", dollars $PATH, parens (test)
HERMES_EOF
echo "$f"')
# Step 2b: Run Claude in print mode (NO --resume — this is a fresh session)
# || true prevents set -e from killing the script on non-zero exit
# timeout 280 prevents runaway hangs
RESP=$(ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=accept-new -o ServerAliveInterval=30 n8n@10.0.0.28 \
"cd ~/claude/hermes_support && timeout 280 /home/n8n/.local/bin/claude -p \"$(cat $QFILE)\" --output-format json --model claude-opus-4-8 --max-turns 30 || true")
# Step 2c: Clean up temp file
ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=accept-new -o ServerAliveInterval=30 n8n@10.0.0.28 "rm -f $QFILE"
# Step 2d: Parse the JSON (handle empty/non-JSON, error_max_turns, iterate modelUsage)
SID=$(echo "$RESP" | python3 -c "
import sys, json
try:
d = json.load(sys.stdin)
except:
print(''); sys.exit(0)
print(d.get('session_id',''))
")
RESULT=$(echo "$RESP" | python3 -c "
import sys, json
try:
d = json.load(sys.stdin)
except:
print('ERROR: non-JSON response'); sys.exit(0)
subtype = d.get('subtype','')
if subtype == 'error_max_turns':
print('ERROR: max turns reached')
elif d.get('is_error'):
print(f\"ERROR: {subtype} - {d.get('result','')}\")
else:
print(d.get('result','(empty)'))
")
IS_ERROR=$(echo "$RESP" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('is_error',False))")
COST=$(echo "$RESP" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('total_cost_usd',''))")
```
**JSON fields available:**
- `session_id` — save this for follow-ups (capture from EVERY response, including errors)
- `result` — Claude's answer text (may be empty on `error_max_turns`)
- `is_error` — true if something went wrong
- `subtype``success`, `error_max_turns` (is_error:false but no result!), `error_budget`
- `total_cost_usd` — track spending
- `num_turns` — how many agentic loops Claude used
- `stop_reason` — why Claude stopped
- `usage` — object with `input_tokens`, `cache_creation_input_tokens`, `cache_read_input_tokens`, `output_tokens`
- `modelUsage` — dict keyed by model name, each with `contextWindow` (total context window size). Iterate keys — don't hardcode the model name.
## Usage & Context Tracking
Two data sources for monitoring Claude's limits. Check them on every turn so you can alert the user BEFORE they hit a wall.
### 1. Context % (free — from the JSON envelope you already parse)
Compute from the same `--output-format json` response. Zero extra HTTP calls. Iterate `modelUsage` keys — don't hardcode the model name.
```bash
# After parsing RESP, compute context %
echo "$RESP" | python3 -c "
import sys, json
d = json.load(sys.stdin)
u = d.get('usage', {})
used = u.get('input_tokens', 0) + u.get('cache_creation_input_tokens', 0) + u.get('cache_read_input_tokens', 0)
mu = d.get('modelUsage', {})
cw = 200000
if mu:
for model_name, model_data in mu.items():
cw = model_data.get('contextWindow', 200000)
break
pct = int(used / cw * 100)
print(f'context_pct={pct} tokens={used}/{cw}')
"
```
**Alert threshold: ≥ 80%** — warn the user. "Context at 84% — wrap this up or start fresh soon."
### 2. Session % + Weekly % + Reset Timers (OAuth usage API — BEST-EFFORT)
Account-wide counters. **Do NOT call every turn** — the endpoint aggressively rate-limits (open issues anthropics/claude-code #31637, #31021). Call at most every ~5 minutes, cache the last good value, and never gate a turn's success on this call. Swallow all errors (429s, timeouts, auth failures).
```bash
# Fetch usage limits
ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=accept-new -o ServerAliveInterval=30 n8n@10.0.0.28 \
TOKEN=*** -r '.claudeAiOauth.accessToken' ~/.claude/.credentials.json)
curl -s https://api.anthropic.com/api/oauth/usage \
-H "Authorization: Bearer *** \
-H "anthropic-beta: oauth-2025-04-20" \
-H "Content-Type: application/json" \
| jq -r '
-H "Content-Type: application/json" \
| jq -r '
"session_pct=\(.five_hour.utilization)% session_reset=\(.five_hour.resets_at)",
"weekly_pct=\(.seven_day.utilization)% weekly_reset=\(.seven_day.resets_at)"'
```
### Step 3: Present Result to User
- `five_hour.resets_at` — UTC ISO-8601 when session counter resets
- `seven_day.utilization` — weekly cap usage (0100)
- `seven_day.resets_at` — UTC ISO-8601 when weekly counter resets
- Token path: `~/.claude/.credentials.json` (Linux plaintext)
- Requires `anthropic-beta: oauth-2025-04-20` header or it 401s
- **Note:** `seven_day` is all-models weekly, not Opus-specific. `seven_day_opus` exists in the response but is null on this plan. The all-models counter is the one that matters for rate limiting.
**Alert thresholds:**
- Session ≥ 80% — warn before starting a fresh session. "Claude session at 87%, resets at 2:49 PM CT — want to wait or use Sonnet instead?"
- Weekly ≥ 90% — critical. "Claude weekly at 94%, resets Tuesday 2:00 PM CT. Consider Sonnet or waiting."
- When Claude is unavailable (error_budget or rate-limited) — report the exact reset time so the user knows when to try again.
### 3. Alert Rules (apply on every ask-claude turn)
| Check | When | Threshold | Action |
|-------|------|-----------|--------|
| Context % | After every response | ≥ 80% | Warn: wrap up or start fresh |
| Session % | Best-effort (~5min cache) | ≥ 80% | Warn + show reset time, offer Sonnet |
| Weekly % | Best-effort (~5min cache) | ≥ 90% | Critical warning + reset time |
| Unavailable | On error_budget/rate-limit | N/A | Report exact reset time |
### 4. Combined One-Shot Script
The deployed files on 10.0.0.28 are two separate scripts in `~/claude/hermes_support/`:
- **ask.sh** — wrapper script. Takes `--resume SID`, `--max-turns N` (default 30), `--qfile PATH`. Writes question to mktemp snapshot, runs claude with `timeout 280 || true`, fetches usage API (best-effort), calls merge.py.
- **merge.py** — JSON merger. Parses claude output + usage API JSON, handles empty/non-JSON gracefully (try/except), extracts session_id/result/is_error/cost/usage fields. No context_pct (cumulative usage is unreliable).
These are the source of truth. The local `scripts/ask_claude.sh` in this skill is a reference copy — the deployed files on 10.0.0.28 take precedence.
Invocation from Hermes:
```bash
# Fresh session
ssh n8n@10.0.0.28 '~/claude/hermes_support/ask.sh --qfile /tmp/my-question.txt'
# Resume
ssh n8n@10.0.0.28 '~/claude/hermes_support/ask.sh --resume SESSION_ID --qfile /tmp/my-question.txt'
```
### Step 3: Present Result to User
Show Claude's response. If Claude asked a clarifying question (visible in the result text), get the user's answer.
### Step 4: Send Follow-ups (WITH --resume)
Same mktemp pattern, but add `--resume <session_id>` to continue the conversation. Always capture session_id from every response (including errors) — a turn can error yet still advance the session.
```bash
# Step 4a: Write follow-up to temp file (mktemp)
QFILE=$(ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=accept-new -o ServerAliveInterval=30 n8n@10.0.0.28 \
'f=$(mktemp /tmp/ask-claude-q-XXXXXX.txt) && cat > "$f" << "HERMES_EOF"
FOLLOWUP QUESTION TEXT
HERMES_EOF
echo "$f"')
# Step 4b: Resume the same session
RESP=$(ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=accept-new -o ServerAliveInterval=30 n8n@10.0.0.28 \
"cd ~/claude/hermes_support && timeout 280 /home/n8n/.local/bin/claude -p --resume SESSION_ID \"$(cat $QFILE)\" --output-format json --model claude-opus-4-8 --max-turns 30 || true")
# Step 4c: Clean up + parse JSON again
ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=accept-new -o ServerAliveInterval=30 n8n@10.0.0.28 "rm -f $QFILE"
```
Parse the JSON again. The session continues — Claude remembers the full conversation.
### Step 5: Report Back
Summarize Claude's findings in 1-3 sentences. Note any .md files saved in `~/claude/hermes_support/`.
## Error Handling
```bash
# Check for empty response (timeout/crash/OOM)
if [ -z "$RESP" ]; then
echo "ERROR: Claude returned empty response (timeout/crash/OOM)"
fi
# Check for non-JSON response
echo "$RESP" | python3 -c "import sys,json; json.load(sys.stdin)" 2>/dev/null || {
echo "ERROR: Claude returned non-JSON. Raw: $(echo "$RESP" | head -c 500)"
}
# Check is_error in JSON
if [ "$IS_ERROR" = "True" ]; then
echo "Claude reported error: $RESULT"
# subtype tells you why: error_max_turns, error_budget, etc.
fi
# Detect error_max_turns specifically (is_error:false but no result field)
SUBTYPE=$(echo "$RESP" | python3 -c "import sys,json; print(json.load(sys.stdin).get('subtype',''))")
if [ "$SUBTYPE" = "error_max_turns" ]; then
echo "ERROR: max turns reached. Retry with higher --max-turns or simplify."
fi
```
**Common errors:**
- `error_max_turns` — question too complex, increase `--max-turns` or simplify. Note: `is_error` is `false` but `result` is empty — detect via `subtype`.
- `error_budget` — hit spending cap, check Pro limits
- Non-zero exit code — SSH failure, Claude not installed, or process killed. `|| true` keeps the script alive so you can still parse the JSON.
- Empty response — timeout (increase from 280s), OOM, or crash
- Non-JSON response — SSH dropped mid-call, Claude crashed before writing output
## Sonnet Fast-Path
For routine questions where Opus-level reasoning is overkill, offer the user a faster option:
```bash
/home/n8n/.local/bin/claude -p "question" --output-format json --model claude-sonnet-4-6 --max-turns 10 || true
```
Sonnet is 3-5x faster and cheaper. Use for: factual lookups, simple analysis, quick checks.
## Pitfalls
- **CRITICAL: Never inline questions in SSH commands.** Shell metacharacters (`"`, `$`, `(`, backticks) will break. Always write to a temp file via `mktemp` first, then `claude -p "$(cat $QFILE)"`.
- **Always use `--output-format json`** — without it, you get plain text and can't extract session_id for follow-ups.
- **Always set `--max-turns`** — prevents runaway loops. 30 is a good default for analysis tasks (20 was too tight with CLAUDE.md forcing web-search-first + .md writes).
- **Always pass `--model claude-opus-4-8`** — pin the model explicitly. Profile defaults drift silently. The old "do NOT pass --model" rule was wrong.
- **Always use `|| true` after the claude command** — `set -e` kills the script on non-zero exit, but errors still produce parseable JSON you need.
- **Always wrap with `timeout 280`** — prevents runaway hangs that exceed SSH's 300s timeout.
- **Always add `-o ServerAliveInterval=30`** to SSH commands — Opus turns run 45-90s and SSH can silently disconnect.
- **Use full path `/home/n8n/.local/bin/claude`** — bare `claude` only works in interactive shells that source `.bashrc`.
- **Session IDs expire** — sessions last ~5 hours. After that, `--resume` fails. Start fresh.
- **No session cleanup needed** — sessions auto-expire. Don't try to "close" them.
- **JSON parsing** — use Python's `json.load(sys.stdin)` not jq, to handle control characters in Claude's output. Wrap in try/except for non-JSON responses.
- **SSH timeouts** — Opus can take 60-120 seconds. Set generous timeouts (300s for SSH, 280s for claude process).
- **CRITICAL: Ask before probing.** Do NOT SSH into 10.0.0.28 without asking the user first.
- **CLAUDE.md is the source of truth** — if behavior seems off, check `~/claude/hermes_support/CLAUDE.md` on 10.0.0.28.
- **Tool sanitization: `$VARNAME``***`** — Hermes tooling (write_file, skill_manage patch, execute_code) sanitizes `$VARIABLE` patterns to `***` at write time. When editing the combined script or any code block containing shell variables, use Python's `chr(36)` to construct `$` and write via terminal heredoc or Python file I/O. Example: `dollar = chr(36); script = f'TOKEN={dollar}(jq ...)'`. Same issue affects `|` (pipe) — use `chr(124)` if it gets mangled to `|`.
- **Capture `session_id` from EVERY response** — including errors. A turn can error yet still advance the session. If you only store it on success, resume breaks.
- **Detect `error_max_turns` via `subtype`** — it has `is_error:false` but no `result` field. Your parser will emit empty answers if you don't check subtype explicitly.
- **Iterate `modelUsage` keys** — don't hardcode `modelUsage['claude-opus-4-8']`. The key changes with the model id. Use `for model_name, model_data in mu.items(): cw = model_data.get('contextWindow', 200000); break`.
- **Usage API is best-effort only** — it aggressively rate-limits. Call at most every ~5 min, cache last good value, swallow all errors (429s, timeouts). Never gate a turn on this call.
- **Use `mktemp` for temp files** — `/tmp/ask-claude-q.txt` is a fixed path that concurrent Hermes sessions clobber. Use `mktemp /tmp/ask-claude-q-XXXXXX.txt` and `trap "rm -f $QFILE" EXIT`.
- **Shell metacharacters in questions break heredoc** — when the question contains quotes, parentheses, dollar signs, or other shell-special characters, the `mktemp` + heredoc pattern on the remote host will fail with syntax errors (bash tries to interpret the content). **Workaround:** write the question to a local temp file, `scp` it to the remote host, then use `ask.sh --qfile` with the remote path. Example:
```bash
# Write question locally
write_file("/tmp/ask-claude-q.txt", content=question)
# Copy to remote
terminal("scp /tmp/ask-claude-q.txt n8n@10.0.0.28:/tmp/ask-claude-q.txt")
# Run via ask.sh
terminal("ssh n8n@10.0.0.28 '~/claude/hermes_support/ask.sh --qfile /tmp/ask-claude-q.txt'")
```
This avoids all shell escaping issues. The `ask.sh` wrapper handles mktemp snapshot + claude invocation + merge.py internally.
## Claude's CLAUDE.md (for reference)
The consulting workspace at `~/claude/hermes_support/CLAUDE.md` documents print mode as the operating procedure and includes behavior rules: zero fluff, SearXNG-first, save findings as .md, stay on topic, ask when unclear, wrap up when done.
## Verification
```bash
# Quick smoke test
ssh -i ~/.ssh/id_ed25519 -o StrictHostKeyChecking=accept-new -o ServerAliveInterval=30 n8n@10.0.0.28 \
'cd ~/claude/hermes_support && timeout 60 /home/n8n/.local/bin/claude -p "Respond with exactly: ASK_CLAUDE_OK" --output-format json --model claude-opus-4-8 --max-turns 1 || true'
```