---
name: verifying-user-claims
description: Verify on disk and over the network before persisting a user's "I just set this up" claim to memory, skills, or reports. Also covers the inverse case — when the user references prior work, a project, or a pipeline you don't recognize, investigate the obvious surfaces AND consider the local-vs-VPS split as the most likely cause before confidently asserting absence. Trigger whenever the user says "I configured X", "I set the token", "remote is at URL Y", "we did it", "run the X pipeline on this Y", or otherwise asserts either a state change OR references prior collaborative work that isn't in your current context.
---

# Verifying User Claims

When a user asserts a state change ("I set up X", "remote is at Y", "the token is in Z", "we did it"), do NOT take it on faith — verify on disk and over the network before saving it to memory, embedding it in a skill, or reporting it back as done. Users often remember the *intent* of a change and not whether it was actually persisted, or they conflate a different machine / profile / shell with the current one.

## Why this exists

In one session, a user said they "got the Gateway to use Remote on https://2.25.172.164 a token". I almost saved to memory: *"remote gateway is at this URL, auth via Bearer, the token is X"*. The reality:

- The URL WAS live and the right service (verified via /openapi.json).
- The "token" I'd saved was actually scraped from `window.__HERMES_SESSION_TOKEN__` in the *public dashboard HTML* — not a real configured secret.
- The local box had **no** reference to 2.25.172.164 in config.yaml, auth.json, env vars, or the running gateway's state.

Half of the claim was true, half wasn't. Saved alone, the memory entry would have propagated a false mental model into every future session.

## The verification protocol

Run these checks before persisting *anything* the user asserted about current state:

1. **Search config files for the literal string.** `grep -n '<URL or identifier>' ~/.hermes/config.yaml ~/.hermes/auth.json` and any other relevant config. If it's not there, the user may be remembering a different machine, a different profile (`~/.hermes/profiles/<name>/`), or a shell-only env var.
2. **Search env vars.** `env | grep -iE '<keyword>'` — but be aware this only catches the *current* shell. A token set in yesterday's terminal is gone.
3. **Check runtime state.** For services: `cat ~/.hermes/<service>_state.json`, `ss -tlnp`, the relevant process's CLI flags via `/proc/<pid>/cmdline`. For other long-lived config: the live DB.
4. **Probe the network endpoint independently.** If the claim includes a URL, hit it with curl. See `references/probing-remote-hermes-gateway.md` for the recipe if it's a Hermes gateway; otherwise just confirm it serves and the user-described behavior actually works.
5. **Distinguish the user's box from the remote.** A common confusion: "I set up the remote gateway" can mean (a) the remote gateway exists and is up, (b) the local gateway is now configured to talk to it, or (c) the local box is the remote (now bound to a public IP). All three are different states with different config footprints.
6. **Windows-side Claude Code is a first-class config surface, not a footnote.** When the user says "I added the key into Claude Code via my local terminal," treat that as a real persisted state on Windows, not a shell-only ephemeral. Check `C:\Users\Rob\.claude\settings.json`, `settings.local.json`, and the env of the Windows terminal session that launched Claude Code for `OPENAI_API_KEY` / provider blocks. VPS absence of the same key does not mean the user didn't set it — it means the VPS doesn't have it.
7. **"I set up X" can mean a two-piece stack where one piece is missing.** A specific instance: Julian Goldie's "Agentic OS" dashboard has a "Free Claude" / "FCC" card that polls a local proxy (`fcc-server`, the [free-claude-code](https://github.com/Alishahryar1/free-claude-code) project) on `127.0.0.1:8082`. The card shows "Offline / fcc-server down" when the proxy is not running. The user said "I set up Free Claude" meaning they ran the dashboard installer. The installer does NOT start `fcc-server`; it just installs the executables. To verify Free Claude is actually working, check BOTH ends: is `fcc-server` running, AND is a model configured in its Admin UI (default: Ollama). If you only check one, you'll say "set up" when only the installer ran. The same shape applies to anything "local-proxy + local-backend" — confirm the proxy, the config, AND the backend all exist before claiming a green light. See `references/free-claude-code-fcc-server.md` for the full fcc-server reference (paths, port, env, provider namespace, common MODEL= mistakes).

6a. **Status card "Live" / "Online" badges are not proof that the chat path works.** A specific failure mode that fooled me in 2026-07-16: the Agentic OS "Free Claude" card flipped from red "Offline / fcc-server down" to green "Live" with the right model name (`llama-3.3-70b-instru...`) showing. A chat message returned `(no output)`. The reason: the card polls `GET /health` every ~3s and that endpoint always returns 200 if the process is up. The card does NOT verify that a `POST /v1/messages` actually reaches the proxy and produces a response. Verification protocol for any "card says Live but chat is empty" situation:
   - **Confirm the request method.** Look at the proxy's stdout — if it's full of `GET /health` but zero `POST /v1/messages`, the card's poll path is the only thing reaching the proxy. Chat is going somewhere else (wrong port, wrong URL, or the chat layer is bailing before sending).
   - **Hit the chat endpoint directly with curl/Invoke-WebRequest**, bypassing the UI. Use the exact bearer token and headers the UI would send. If the direct call works and the UI doesn't, the bug is in the UI's request formation. If the direct call also fails, the bug is in the proxy or its upstream.
   - **Watch for the proxy to die under load.** `fcc-server` exited cleanly (Uvicorn "Press CTRL+C to quit" banner, no traceback) after about 60 seconds under a 3-second health-polling cadence plus a single chat attempt. The "stopped, not crashed" failure mode is invisible in stderr; the only signal is `Get-Process fcc-server` returning empty after a known-good start. See `windows-launch-diagnostics` for how to detect this (sample process + port on a loop, not a single check).
7. **Light-hardware local-LLM ceiling.** When a user is on a light laptop (e.g. 12 GB RAM, integrated GPU), local Ollama with anything above ~2-3 GB is risky, and pulling a 7B+ model will fail or starve the system. Before recommending `ollama pull <large-model>`, check RAM with `Get-CimInstance Win32_ComputerSystem` (Windows) or `free -h` (Linux). For a 12 GB laptop the realistic local ceiling is `llama3.2:2b` (2 GB) or `qwen2.5:3b` (2 GB). Anything bigger belongs on the VPS. The "missing-out" feeling the user has when a local model is dumb is real but unfixable on that hardware; recognize the ceiling and route heavy work to the remote.

## When the user references prior work, a project, or a pipeline you don't recognize

The same instinct that says "verify before saving" applies in reverse when the user references prior work that doesn't show up in your current context. The failure mode this session demonstrated:

User said: *"Run the resume pipeline on this JD — part of the Job Seeker project"*. The agent searched `~/.hermes/projects/`, ran `session_search` for "Job Seeker resume pipeline", and reported: *"I have no record of a Job Seeker project... I don't want to invent one and pretend I ran it."* The user pushed back: *"so you have no skills or session memory of a project we've worked for over 100 hours over the last 2 months?"* The work existed the whole time — at `C:\Users\Rob\Business_Projects\Project_1_Job_Seeker\` on the Windows laptop.

### Why the default-to-absence reflex is wrong here

The user's mental model is "we did this work together; it should be in *our* shared state." Your mental model is "what I can see right now in this session/profile/filesystem." Those are not the same thing, and a hard "I don't have it" without an investigation is a confident answer to the wrong question.

### The protocol when prior work is referenced but not visible

1. **Check the obvious local surfaces first** — `~/.hermes/projects/`, `~/.hermes/Business_Projects/`, `session_search` across multiple phrasings, `~/.hermes/skills/`, the kanban DB. Run a broad filename + content search for the topic (`find ~/.hermes -maxdepth 4 -type f \( -name "*.md" -o -name "*.yaml" -o -name "*.json" \) | xargs grep -l -iE '<topic>'`). If anything turns up, read it before saying "I don't have it."
2. **Consider the local-vs-VPS split as the most likely cause** when nothing turns up. If the architecture is "VPS is single source of truth, Windows runs Hermes Desktop as a pure client" (this user's setup), then prior work that pre-dates the consolidation lives on the Windows box and is invisible from the VPS terminal. Per the standing memory rule, do not poke around in `C:\Users\Rob\VPS_Agents\Robs Hermes\` without an explicit ask, but DO ask the user for the path on Windows.
3. **Ask the user where the project lives** — don't confidently assert absence. A correct framing is: *"I checked the obvious places on the VPS and didn't find it. Per the local-vs-VPS split, it may be on the Windows side. Where does the project live?"* — not *"I don't have any record of this project."*
4. **Distinguish "no record in this session" from "doesn't exist"** in your reply. The first is true and useful. The second is a confident claim about a system you can only see partially.

### Pitfalls specific to this case

- **Don't say "I don't have a record of X" as a final answer.** It's almost always partial — you have a record of *what you can see right now*, which is not the whole picture.
- **Don't propose to build the pipeline from scratch before asking.** The user almost certainly has a pipeline config / template / prior output somewhere; rebuilding from scratch loses hours of polish.
- **One narrow `session_search` query is not an exhaustive search.** If the first query returns nothing, try 2–3 rephrasings, broaden the query, and do the filesystem sweep described in step 1. Only then escalate to "I can't find it — where does it live?"
- **When the user gets frustrated, the right move is to verify the explanation they offer**, not to defend the original claim. In this case, the user's "is this due to the fact we moved you to the VPS and local?" was the correct diagnosis — accept it and move on to recovery.

### What to do once you find the project (or the user points you at it)

- If the artifacts live on Windows, you'll need them on the VPS to read them. See the companion skill `windows-to-vps-file-transfer` for the three transfer methods (WinSCP, `scp` with password, hPanel file manager + tarball) and the order of preference.
- Read the pipeline spec / config / template before asking the user to re-explain what the pipeline does. The project files are the source of truth — older session memory of "what we did" doesn't transfer across the local-to-VPS split.
- If sensitive content is involved (PII, tokens, addresses), warn before pasting in chat and recommend moving files instead of inlining. The user's standing memory rule on this: tokens were burned by being pasted in chat before.

### Session recovery: browse first when the user says "it was recent" (2026-07-20)

When the user asks you to "recover the last session" or references something "from 2 minutes ago," **keyword search is the wrong first move.** `session_search(query=..., sort='newest')` still ranks by FTS5 relevance — a week-old session with the exact keywords will beat the actual recent session with vaguer content. In one case, searching for "latency Agentic OS" surfaced a July 12 session when the user meant the current session from 2 minutes prior.

**The browse-first protocol for recent-session recovery:**

1. **Call `session_search()` with no arguments** (browse mode). This returns the most recent sessions chronologically: titles, previews, timestamps. Scan the top 3-5 for the one that matches the user's description.
2. **If the top hits don't match, THEN fall back to keyword search** with `sort='newest'` — but treat the results skeptically. Check the `when` field on every hit; if it's older than the user claims, keep looking.
3. **Read the session by ID** (`session_search(session_id=...)`) once you've identified the right one. Don't rely on the discovery snippet alone — it may be truncated or highlight the wrong message.

**Why this works:** Browse mode is pure chronological ordering with no relevance ranking. It can't be tricked by keyword density. For "recent" requests, chronology is the ground truth.

**Pitfall:** The user may say "last session" but mean "the last session about X topic" — not the chronologically last session. If browse shows a session from 2 minutes ago but it's about a different topic, ask the user to clarify before diving in.

### Canonical "where does this live" audit (Windows-side, 2026-07-14)

When the question is broader than one project — "what other work is stranded on local?" or "is there anything else I'm forgetting about?" — run this single PowerShell block on the Windows side. It inventories the standard candidate roots, prints file count + size per root, and a one-level-deep directory listing. Safe, read-only, and surfaces the obvious places without poking anywhere sensitive.

```powershell
# File count + size per root
$roots = @(
  "C:\Users\Rob\Business_Projects",
  "C:\Users\Rob\.hermes",
  "C:\Users\Rob\AppData\Roaming\hermes",
  "C:\Users\Rob\Documents",
  "C:\Users\Rob\Desktop",
  "C:\Users\Rob\Downloads",
  "C:\Users\Rob\VPS_Agents"
)
foreach ($r in $roots) {
  if (Test-Path $r) {
    $count = (Get-ChildItem -Recurse -File -Force $r -ErrorAction SilentlyContinue | Measure-Object).Count
    $size  = (Get-ChildItem -Recurse -File -Force $r -ErrorAction SilentlyContinue | Measure-Object Length -Sum).Sum
    Write-Host ("{0,-60} {1,8} files  {2,12:N0} KB" -f $r, $count, ($size/1KB))
  }
}

# One-level-deep directory listing for the candidate roots
foreach ($r in $roots) {
  if (-not (Test-Path $r)) { continue }
  Write-Host ""
  Write-Host "=== $r ===" -ForegroundColor Cyan
  Get-ChildItem -Path $r -Force -ErrorAction SilentlyContinue |
    Where-Object { $_.PSIsContainer } |
    Select-Object Name, LastWriteTime |
    Format-Table -AutoSize
}
```

**Notes on the candidate list and exclusions:**
- `C:\Users\Rob\AppData\Local\hermes` is intentionally **excluded** — on Rob's install it's 5.4GB / 150k files of Electron cache + `node_modules` style dumps. Adding it will print numbers but won't surface real work.
- `C:\Users\Rob\VPS_Agents\Robs Hermes` is **listed** (not excluded) because it shows up in the audit, but per the standing memory rule the contents are read-only without explicit ask. Just print the top-level, don't recurse.
- After the audit, ask the user which folder(s) to push, not which files inside — they know the project names; you don't yet.
- The expected pattern after this audit: `Business_Projects` shows multiple `Project_*` folders; the user's primary active work is one of them (per memory: Project_1 for Job Seeker, Project_3 for RRR). Other `Project_*` folders are usually smaller spec/early-stage folders, not stranded mature work.

---

## When the user dismisses a tool as "not useful" — verify before agreeing

**Worked example:** user said *"I installed Ollama long after Hermes and this Agentic OS, I can uninstall it...not the useful to me anyway"*. The agent almost agreed. Then the agent ran the verification protocol:
- `Get-Process ollama` → 2 processes running, started 8:07 AM, well before the current session.
- A `pythonw.exe` started 7 seconds before Ollama → strong evidence something wired the two together.
- The Agentic OS dashboard's troubleshooting docs explicitly say *"Voice building (Agent Factory) says 'local model not reachable' — Ollama isn't running."*
- The dashboard's local "Free Claude" path defaults to Ollama as the provider.

The dismissal was a guess. Ollama is a backend for the user's local dashboard. Removing it would break the dashboard's "local model" features and force every AI call onto Nous, contradicting the user's own goal of "manage inference costs from that platform." The right move: push back with the evidence, leave Ollama installed, move on.

**The pattern:** when a user asserts a tool is "not useful," do the same verification you would for "I set up X." Check process list, start times, dependents, config references. If anything points at the tool, the dismissal is wrong, and silently agreeing causes a problem the user will only discover when something breaks later. The 30 seconds of verification is cheaper than the support session that comes after a wrong agreement.

## Verifying remembered docs/config claims against the code (the inverse case)

The verification protocol isn't only for user claims about *current state* — apply it to any remembered "the docs say X" or "there's a hard cap of N" claim too, including your own. Worked example 2026-08-03: the claim "Hermes memory has a 5,000-char hard cap" had been treated as established fact across sessions. Verification: `grep -rn "memory_char_limit" venv/.../hermes_cli/*.py` → only hit is the default value in `config.py`; `memory_tool.py` + `agent_init.py` read `memory.memory_char_limit` from config and pass it **unclamped** into `MemoryStore`. The 5,000 figure appears nowhere in the enforcement path, the docs, or the source tree — it was pure folklore. The user's `memory_char_limit: 8000` was valid the whole time; only the process restart was needed for new sessions to pick it up.

**The method:** for any "limit/cap/default" claim about Hermes internals, grep the *enforcement* path (the tool/init code that reads the config key), not just the config schema. A value in `config.py` with no clamp in the consumer is a soft default, not a cap. Numbers remembered from docs, changelogs, or prior sessions calcify fast — 60 seconds of grep beats propagating a false constraint for months.

## What to do when verification fails or is partial

- ✅ What you confirmed.
- ⚠️ What you couldn't confirm, and exactly which checks failed.
- Then **ask** for clarification rather than guessing. Three good questions:
  1. Is the remote on a *different machine*, or is the local box now serving on the public IP?
  2. Where exactly did you set the token — CLI flag, env var in a running shell, or in a config file I should look harder at?
  3. Is the goal to point the local gateway at the remote, or to use the remote's dashboard directly?

## Pitfalls

- **Scraped tokens from public HTML are not secrets.** If you find a "token" in a `/login` page's `__SOMETHING__` global, that is the *session cookie value* or a per-page-load CSRF token, not a configured API credential. Do not save it as a durable secret.
- **Different Hermes profiles don't share memory or state.** Always check `~/.hermes/profiles/*/` if the user mentions a specific profile. The default profile's `config.yaml` is not the whole story.
- **Local-vs-remote confusion.** A gateway running on `[IP_ADDRESS]:9119` on the local box and a remote at `https://X.X.X.X/` are *separate instances* unless the local one is explicitly configured to proxy to the remote. Don't conflate them.
- **Curl to raw IPs triggers an approval gate.** Commands like `curl https://[IP_ADDRESS]/...` get flagged as "[MEDIUM] URL uses raw IP address." This is normal — the user can approve, but warn them in advance so they're not surprised by the prompt. Tip: the first probe in a session usually needs approval; subsequent ones to the same host are remembered.
- **Trust the bytes on disk, not the redaction-filtered display.** When a chat output shows you content of a file, the redaction filter may strip/transform tokens (em-dashes, names, addresses) before you see them. The agent's *displayed* view of a file is NOT the file's actual content. Concretely hit 2026-08-12: I patched `social-media-agent/SOUL.md` to add a heading + blockquote pointer, then looked at `head` output and thought the heading and blockquote had been merged into one line because the display showed `# Soul — Social Media Agent profile> **Shared network identity**...`. Started writing "fix" patches. The actual bytes on disk (verified with `cat -A`) were fine — `M-bM-^@M-^T` is the UTF-8 em-dash, displayed as nothing/collapse-able by the filter. **Rule: when a `head`/`cat`/`read_file` output looks malformed, run `cat -A` (or `od -c | head`) on the file before patching.** Patching a file that's actually fine makes it broken.
- **PowerShell: don't use angle-bracket placeholders in commands the user runs.** PowerShell parses `<` as a redirection operator and throws `ParserError: The '<' operator is reserved for future use.` This applies even to obvious placeholders like `ssh <user>@<host>`. Either (a) use the real value, (b) write a PowerShell-friendly form like `ssh $env:USER@<hostname>` with a clear "replace `<hostname>`" callout, or (c) ask for the value first and issue the command in the next turn. The same rule applies to wrapping multi-step PowerShell blocks — always wrap with a "Copy and paste this whole block into PowerShell:" header and separator so the user knows it's one runnable unit, not split-it-up reference material. See `operating-hermes-gateway` "How to format commands the user runs in PowerShell" for the full format.
- **Windows SmartScreen "not Microsoft verified" warnings on legitimate open-source installers are not a danger signal.** WinSCP, PuTTY, Notepad++, 7-Zip, Git for Windows, and most non-store apps trigger the same "unknown publisher" warning. The right safety check is: is the installer from the official site, and is it code-signed? (WinSCP's installer is signed by its author — Properties → Digital Signatures will show it.) The way through the warning is "More info" → "Run anyway". If the user is uncomfortable, fall back to built-in alternatives (PowerShell OpenSSH `scp` is preinstalled on Windows 10/11 and doesn't trigger SmartScreen) or to the hPanel file manager. Don't pre-emptively scare the user about a normal Windows security prompt.
- **When the user signals done, stop after one acknowledgment.** Signals: "let's forget about this," "I'll get back to X," "moving on," "go hit [other task]." The pattern this protects against: the user closes a thread, and the agent keeps generating follow-up questions, audit tangents, or "while we're here" suggestions. That burns the trust the user placed in saying "go do Y" — they will re-engage on the current topic if they want more. The right move after a done-signal: one short acknowledgment, possibly one concrete action (e.g. "filing this, on to RRR"), and then stop. Do not pivot to a "by the way" or "before you go, one more thing" follow-up. Concrete miss from a session: user said "let's forget about this... and I'll get back to work on Real Results Ready punch list," and the agent kept running for several more exchanges on the stranded-work question before stopping. The skill's verification protocol doesn't end with "I confirmed something" — it also covers "I confirmed the user is done with this topic, so I stop."
- **Trust the existing skills over your own reading.** The `verify-file-transfers` skill already says exit codes lie and file counts don't. When a `tar -czf` call reports exit 0 and a 1.1MB tarball, the skill says "verify the tarball contents before scp." The right move is to verify, not to declare success. Same for any other transferable rule the user has already seen you write down — the next time the pattern shows up, run the rule, don't re-derive it. Concrete miss from a session: a `tar -czf` in a loop with the staging-dir pattern produced a 1.1MB archive that contained only 8 of the expected 23 files. The `verify-file-transfers` skill had the fix; the agent did not apply it and had to be pushed through three re-extract cycles before applying it. The cost of "I'll just trust the exit code this once" is always more rework than the 10 seconds the verification would have taken.

## Embedding this in your workflow

Whenever the message starts with "I configured…", "I set…", "remote is…", "we did it", or otherwise asserts a state change:

- Default to verification before memory writes. The 30 seconds of grep/curl is cheaper than 6 months of stale memory.
- If you do write to memory, mark it explicitly as a *user-asserted fact pending verification* if you can't fully confirm it, OR don't write it at all and ask the user instead.
- Pair any "✅ done" report with the exact `grep` / `curl` / `cat` you used to confirm, so the next session can re-verify cheaply.

## Vetting third-party setup docs the user pastes in (same protocol, outward-facing)

The verification instinct applies to EXTERNAL how-to docs the user brings in (blog posts, AI-generated setup guides, "read this" walkthroughs), not just user claims. Rob's standing expectation (memory, 2026-08-06): line-by-line vet against his actual environment BEFORE adopting anything. The pattern that has worked:

1. **Confirm the product exists and is maintained** — find the canonical repo/site; check stars/commit recency. Watch for npm-squatter traps (e.g. GBrain: real tool is `github:garrytan/gbrain`; the npm `gbrain` package is unrelated — the repo's own INSTALL doc warns about it).
2. **Check each command literally** — pasted docs often arrive with URLs stripped by the intermediate tool (e.g. `curl -fsSL | bash`, `git clone && cd <repo>` — both missing their URLs). Broken-on-arrival commands are a strong signal the whole doc was machine-generated or mangled.
3. **Flag existing infra the doc reinvents** — e.g. the doc prescribed setting up Syncthing when Syncthing was already running and syncing the exact folder. Step one of any "set up X" doc review is "what of this already exists?"
4. **Flag wrong-format config snippets** — a doc showing Claude-Code `{"mcpServers"}` JSON for a Hermes install (Hermes wants `mcp_servers:` in config.yaml).
5. **Surface hidden cost defaults** — tools that silently default to the most expensive mode (GBrain's `tokenmax` search mode, 25× cost spread vs `conservative`) get called out BEFORE install, with the choice presented to Rob as his call.
6. **Present verified-vs-wrong as a comparison table; never adopt the doc's architecture wholesale.**

## Related

- `references/free-claude-code-fcc-server.md` — full fcc-server reference (paths, port, env, provider namespace, common MODEL= mistakes). **Now carries a status banner: fcc-server is redundant for Claude-shaped traffic since Rob upgraded to Claude Pro — use the official CLI + OAuth instead.**
- `references/claude-code-pro-oauth-setup.md` — Claude Code official CLI install + Pro OAuth sign-in walkthrough for Connie. Includes the first-run wizard, `/status` verification, the npm-global-not-writable warning fix, and the benign `hermes-vps-root.crt` CA oddity.
- `references/probing-remote-hermes-gateway.md` — concrete curl recipe for verifying a user-claimed remote Hermes Agent gateway.
- `operating-hermes-gateway` — companion skill for what to do *after* verification: stopping, stabilizing, picking the right control commands per platform, and the single-gateway architecture that the user is most often trying to reach. If the user's claim is "I set up the remote gateway", the next step after this skill is usually that one.
