- 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.
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:
- Read the file locally with
read_fileorterminal cat > /tmp/file.txt - Inline the content in the question as a fenced block
- 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-8explicitly. 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
|| trueafter claude invocation.set -ewould kill the script on non-zero exit, but the error JSON is still parseable. - Always use
mktempfor temp files on the remote./tmp/ask-claude-q.txtis a fixed path that concurrent Hermes sessions clobber.
Multi-turn session lifecycle
- First
ask-claudecall 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 —--resumewill fail. - For maximum continuity, capture
session_idin 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-turnsor simplify.is_error: false, but noresultfield — checksubtypeexplicitly.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.shreturns{"is_error": true, "error": "no Claude output"}but directclaude -pworks — 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.