Files
hermes-skills/ask-claude/SKILL.md
Hermes Agent 1533a6e8c3 ask-claude v2.6.1, ask-hermes v4.2.0, ask-dev v1.1.0, create-plan v1.1.0
- ask-claude v2.4.0 -> v2.6.1: Added mandatory disagreement scan (Step 3.5) with web-reference demand + internal consistency check. Updated callouts for the paste-vs-path rule (now HARD RULE, not just example).
- ask-hermes v4.1.1 -> v4.2.0: Added mandatory disagreement scan mirroring ask-claude.
- ask-dev v1.0.0 -> v1.1.0: Added mandatory disagreement scan mirroring ask-claude.
- create-plan v1.1.0 (new): Multi-agent plan-build dispatcher. Trigger phrases, 10-Q brief script (min as needed, max 10), curator dispatch, capture/handoff format, all 6 failure modes. Companion references: curator-framing-prompt.md, deep-research-dispatch-template.md, ask-claude-validation-template.md, structured-brief-10q.md.

5 rounds of Claude validation on the design plan; final SHIP from ask-hermes peer review.
2026-07-06 12:51:02 -05:00

27 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.6.1 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

Primary method (scp + ask.sh) — works with ANY question content, no shell escaping issues:

Step What Command Pattern
1. Write question Local temp file write_file("/tmp/ask-claude-q.txt", content=question)
2. Copy to remote scp to 10.0.0.28 terminal("scp /tmp/ask-claude-q.txt n8n@10.0.0.28:/tmp/ask-claude-q.txt")
3. Run via ask.sh Fresh session (no --resume) terminal("ssh n8n@10.0.0.28 '~/claude/hermes_support/ask.sh --qfile /tmp/ask-claude-q.txt'")
4. Follow-up WITH --resume <sid> terminal("ssh n8n@10.0.0.28 '~/claude/hermes_support/ask.sh --resume SID --qfile /tmp/ask-claude-q.txt'")
5. Done Process exits. Session auto-expires in ~5h. Cleanup temp files if desired.

Legacy method (mktemp + heredoc) — ONLY for simple questions with zero shell metacharacters. Breaks on ", $, (, ), backticks. The scp method above is preferred for all real questions.

Critical: ask.sh handles --output-format json, --model claude-opus-4-8, --max-turns 30, timeout 280, and || true internally. Pin the model explicitly — profile defaults drift silently.

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
  • Adversarial consulting — ask for advice, then ask Claude to push back on its own advice with web searches. See references/adversarial-consulting.md for the pattern.
  • Plan validation — adversarial review of an implementation / integration / skill plan. See references/plan-validation-prompt.md for the proven prompt template (workshopped Jul 6 2026).

Critical Evaluation — You Are the Final Authority

Claude's advice is input, not a decision. You are the engineer — you own the code, the system, and the final call. Claude is a consultant who cannot see your files, run your commands, or verify its own claims. Treat every recommendation as a hypothesis to be tested, not a conclusion to be implemented.

Mandatory before acting on Claude's advice:

  1. Do your own reasoning first. Before relaying Claude's answer, ask yourself: does this make sense? Is there a simpler way? Would this actually work in our environment?

  2. Push back when Claude is wrong. If Claude suggests something that doesn't fit — wrong tool, overengineered solution, unverified claim — tell it so explicitly in the next turn. "That won't work because X. Do a web search for alternatives." Claude corrects itself well when challenged; it doubles down when unchallenged.

  3. Do your own web searches to validate. Claude's SearXNG searches are real but its interpretation may be wrong. For any claim about API behavior, version compatibility, or tool capabilities, run your own mcp_searxng_searxng_web_search and compare. Claude has been wrong about Qdrant endpoints, Python behavior, and shell patterns in this exact session.

  4. Involve the user when there's genuine ambiguity. If Claude and you disagree after a round of pushback, or if the right approach depends on constraints only the user knows, surface the disagreement with both positions stated clearly. Don't silently pick Claude's answer over your own judgment.

  5. Watch for overcomplication. Claude's default mode is thoroughness, which can become overengineering. If Claude proposes a multi-step protocol with atomic renames, CAS semantics, and lease expiration — ask: "Is there a simpler way? Can we solve this with a 50-line Python script instead?" The best fix is usually the simplest one that actually works.

  6. Pre-empt overcomplication in the prompt. When asking Claude to validate a plan or design, add a constraint like: "CRITICAL CONSTRAINT: Do NOT propose new features, new architecture, or new complexity. Your job is to find FLAWS in what's already proposed — not to add more. If you find a problem, say what's wrong and suggest the SIMPLEST fix." Without this, Claude will propose additional layers, new abstractions, and feature creep — exactly what you're trying to avoid in a validation pass.

Claude's Limitations

Claude cannot read your local files. Claude runs on 10.0.0.28 and has no access to /home/n8n/workspace or any other path on your machine. When Claude says "I can't read that file" or "that path doesn't exist here" — believe it. Do not ask Claude to validate a file by path. Instead:

  • For code review: Paste the relevant functions inline in your question. Claude can review pasted code accurately.
  • For design review: Describe the system and paste the key sections. Claude can reason from descriptions if they're specific.
  • For "is this file correct?": You verify it yourself. Claude cannot.

Claude may fabricate plausible-sounding analysis when it can't verify. In this session, Claude confidently claimed the deployed prompt was a "v5 classify-only rewrite" based on a session title it couldn't re-open. That claim was wrong. When Claude prefaces a claim with "from my notes" or "I recall" without being able to re-read the source, treat it as unverified.

Claude's environment is not your environment. Claude has its own tools, its own filesystem, and its own constraints. A command that works in Claude's environment may fail in yours. Always test Claude's suggestions locally before deploying.

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 3.5: Disagreement Scan (MANDATORY before applying)

Do NOT silently accept Claude's recommendations. Do NOT silently drop suggestions you disagree with. Both are failure modes.

For every Claude response, before applying or moving on, do this scan:

1. Disagreement check. Walk through Claude's findings (release-blockers, medium, low, suggestions). For each:

  • Agree + will apply → no action this step
  • Agree + will skip → state explicitly WHY you're skipping (overkill, environment constraint, contradicts operator's standing rule, etc.). Do not just drop it.
  • Disagree → push back. Either (a) decide it's not worth the round-trip cost and state your reason, or (b) send a follow-up turn challenging Claude's claim. If you don't push back on at least one item per multi-finding response, you're accepting Claude's framing wholesale — which defeats the purpose of asking.

2. Web-reference check for weak conclusions. For any Claude claim that is:

  • a tool version, API behavior, library status, or compatibility statement
  • an architecture pattern recommendation
  • a "best practice" assertion
  • any concrete factual claim that the plan or system will depend on

…and where Claude did NOT cite a source URL inline, ask Claude for the reference. Pattern: "What's the source for X? I want a URL I can verify before relying on it." Claude has web search; let it use it. If Claude can't produce a URL, the claim is weak and you should treat it as unverified before applying.

This is the "Web Search Mandate" — Claude should not be the sole source of any concrete factual claim that the build will depend on. If Claude says "X is the latest version" without a URL, push back and demand the source.

3. Internal consistency check. Does Claude's response contradict itself, or contradict your prior turn in this Claude session? If yes, point it out in the next follow-up. Don't apply a contradictory recommendation.

Output format for the scan (use this when relaying Claude's response to the operator):

[Claude's findings as-is]

## My disagreement scan
- **Agreed and applied:** [list]
- **Agreed but skipped:** [item] — [reason]
- **Disagreed and pushed back:** [item] — [what I said]
- **Web references requested:** [items where I asked for a URL]
- **Disagreed silently dropped:** (should be empty — if not, justify)

If Disagreed silently dropped is non-empty, that's a bug. Surface it.

See also: references/peer-response-protocol.md for the full disagreement-scan protocol, web-reference mandate, cross-document desync mitigation, and pre-build verification gate. These patterns generalize beyond ask-claude to any peer dispatch and any multi-section doc edit.

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: Pasted inline beats file paths every time. Claude runs on 10.0.0.28 and has no access to /home/n8n/workspace/, ~/workspace/, or any other local path. "Read the plan at /home/n8n/workspace/.../_plan.md" wastes a turn: Claude searches the wrong filesystem, returns "file not found," and you paid $0.20-$0.35 for nothing. Always paste the artifact inline between clear markers like ==== PLAN BEGINS ==== ... ==== PLAN ENDS ====. The "Claude's Limitations" section above already says this — but the failure mode is costly enough to deserve its own pitfall. File paths in questions should be reserved for files Claude can actually see on its own host (~/claude/hermes_support/...).
  • ask.sh can fail silently while direct claude -p works. The wrapper may return {"is_error": true, "error": "no Claude output (timeout or CLI failure)"} even when Claude is healthy. When this happens, fall back to the direct invocation: ssh n8n@10.0.0.28 'cd ~/claude/hermes_support && timeout 280 /home/n8n/.local/bin/claude -p "$(cat /tmp/ask-claude-q.txt)" --output-format json --model claude-opus-4-8 --max-turns 30 || true'. The wrapper is preferred (it handles usage API + merge.py), but the direct path is the reliable fallback. If the direct path also fails, Claude is genuinely down.
  • 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'