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

20 KiB
Raw Blame History

name, description, version, author, license, platforms, metadata
name description version author license platforms metadata
ask-claude 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. 2.3.0 Hermes Agent MIT
linux
hermes
tags related_skills
claude
opus
consulting
planning
analysis
ssh
print-mode
usage-tracking
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, `
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 "$(cat $QFILE)" --output-format json --model claude-opus-4-8 --max-turns 30
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.

# 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
  • subtypesuccess, 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.

# 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).

# 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:

# 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.

# 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

# 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:

/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 commandset -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:
    # 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

# 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'