Files
hermes-skills/ask-claude/references/ask-claude-payload-pattern.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

6.6 KiB

Ask-Claude Payload Pattern

Session-specific detail for the ask-claude skill: how to send questions to Claude Opus 4.8 on 10.0.0.28 reliably, capture the session_id, handle shell metacharacters, and avoid the filename-collision gotcha that bites when read_file returns a different schema than expected.

Pattern: scp + ask.sh

The ask.sh wrapper on 10.0.0.28 handles mktemp snapshot, claude invocation (--output-format json --model claude-opus-4-8 --max-turns 30, timeout 280, || true), fetches the usage API, and runs merge.py to produce a single JSON envelope.

Why scp, not heredoc: shell metacharacters (", $, (, ), backticks) in the question content will break heredoc-based writes. For ANY non-trivial question write the question to a local temp file, scp it, and pass the remote path to ask.sh --qfile.

# 1. Write question locally
write_file("/tmp/ask-claude-q.txt", content=question)

# 2. Copy to remote
terminal("scp /tmp/ask-claude-q.txt n8n@10.0.0.28:/tmp/ask-claude-q.txt")

# 3. Run via ask.sh
terminal("ssh n8n@10.0.0.28 '~/claude/hermes_support/ask.sh --qfile /tmp/ask-claude-q.txt'")

JSON envelope fields

Always parse these from the --output-format json response:

Field When Use
session_id Always (including errors) Capture for --resume follow-ups
result Success The answer text
is_error Always true if anything failed
subtype Always success / error_max_turns / error_budget
total_cost_usd Always Track spend
num_turns Always How many agentic loops
usage.input_tokens Always Context usage
usage.cache_creation_input_tokens Always Context usage
usage.cache_read_input_tokens Always Context usage
output_tokens Always Context usage
modelUsage.{model}.contextWindow Always Context window size — iterate keys, don't hardcode
stop_reason Always Why Claude stopped

Critical: 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.

Critical: detect error_max_turns via subtype, not is_error. error_max_turns has is_error: false but no result field. Empty-answer detection requires checking subtype == 'error_max_turns'.

Asking Claude to validate a file

Claude (Opus on 10.0.0.28) has no filesystem access to the operator's machine. When asking Claude to validate a plan, code, or doc:

  1. Read the file locally with read_file or terminal cat > /tmp/file.txt
  2. Inline the content in the question as a fenced block
  3. Tell Claude explicitly the file is pasted inline (not a path to read)

Common failure mode: pasting a path like /home/n8n/workspace/... and asking Claude to validate it. Claude will reply "I can't read that file" and refuse to validate. Always inline.

Filename-collision gotcha (execute_code + read_file)

hermes_tools.read_file inside execute_code returns a different schema than the top-level read_file tool. Inside a script:

from hermes_tools import read_file
r = read_file("/path/to/file")
# Schema: {"status": ..., "message": ..., "path": ..., "dedup": ..., "content_returned": ...}
# NO "content" key. Use the top-level read_file tool, OR use terminal cat.

Workaround patterns:

# Pattern 1: terminal cat to a temp file, then open()
import subprocess
subprocess.run(["cp", "/path/to/file", "/tmp/working.txt"])
with open("/tmp/working.txt") as f:
    content = f.read()

# Pattern 2: shell-out to the file directly
with open("/path/to/file") as f:
    content = f.read()  # works in execute_code without hermes_tools

This gotcha caused patch errors in a multi-file edit session: a patch string was constructed from r["content"] which didn't exist inside execute_code. Switched to subprocess.run + with open() to read reliably.

Pre-dispatch model and config

  • Always pass --model claude-opus-4-8 explicitly. Profile defaults drift silently.
  • Always set --max-turns. Default 30 is good for analysis. Lower for trivial questions; higher (50+) for deep architectural reviews.
  • Always wrap with timeout 280. Prevents runaway hangs. SSH itself has 300s.
  • Always add || true after claude invocation. set -e would kill the script on non-zero exit, but the error JSON is still parseable.
  • Always use mktemp for temp files on the remote. /tmp/ask-claude-q.txt is a fixed path that concurrent Hermes sessions clobber.

Multi-turn session lifecycle

  • First ask-claude call in a Hermes session → fresh session (no --resume).
  • Every subsequent call in the same Hermes session → --resume <session_id>.
  • Sessions auto-expire after ~5 hours of inactivity. No cleanup needed.
  • If the operator starts a new Hermes session (.hermes/sessions/ rotates), the Claude session is also gone — --resume will fail.
  • For maximum continuity, capture session_id in the operator-visible relay text so the operator can resume manually if the Hermes session restarts.

Usage and context alerts

Apply on every turn:

Check Threshold Action
Context % (from JSON envelope) ≥ 80% Warn: "wrap up or start fresh"
Session 5h utilization (OAuth API) ≥ 80% Warn + show reset time, offer Sonnet
Weekly 7d utilization (OAuth API) ≥ 90% Critical warning + reset time
Unavailable (error_budget / rate-limit) N/A Report exact reset time

The OAuth usage API aggressively rate-limits. Call at most every ~5 minutes. Cache the last good value. Swallow all errors (429s, timeouts). Never gate a turn on this call.

Common error patterns

  • error_max_turns — question too complex, increase --max-turns or simplify. is_error: false, but no result field — check subtype explicitly.
  • error_budget — hit spending cap, check Pro limits.
  • Empty response — timeout (increase from 280s), OOM, or crash.
  • Non-JSON response — SSH dropped mid-call, Claude crashed before writing output.
  • ask.sh returns {"is_error": true, "error": "no Claude output"} but direct claude -p works — wrapper has a known failure mode. Fall back to direct: ssh n8n@10.0.0.28 'cd ~/claude/hermes_support && timeout 280 /home/n8n/.local/bin/claude -p "$(cat /tmp/q.txt)" --output-format json --model claude-opus-4-8 --max-turns 30 || true'

When the operator pushes back

After applying Claude's recommendations, ALWAYS emit the disagreement scan (see SKILL.md §3.5). Skipping this is a failure mode — both silently-accepting and silently-dropping are bugs. The scan is mandatory before declaring a Claude turn resolved.