Use when session_search misses a known past session.
Scanned 9/10/2026
Install to Claude Code
npx -y skills add theheavenlyd3mon/hermes-profiles --skill hermes-session-recovery --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Hermes Session Recovery?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/theheavenlyd3mon-hermes-session-recovery)More formats (shields.io, HTML) on the badges page.
---
name: hermes-session-recovery
description: "Use when session_search misses a known past session."
---
# Hermes Session & State Recovery
## Trigger
- `session_search` returns nothing, or the wrong sessions, for something you KNOW
happened (a recent session, a just-reset session, a session whose title you recall).
- A session was `/reset` (or `/new`-continued) and you need the PRE-reset content.
- You need to inspect Hermes state directly (sessions, cron runs, kanban, memory DB).
- You need ground-truth ordering/timing of messages that the FTS index hasn't caught.
`session_search` is FTS5-backed and is the right first call — but it lags, can miss
very recent or just-reset sessions, and its discovery shape sometimes returns 0 rows
for content that IS in the DB. The raw SQLite store under `~/.hermes/` is ground truth.
Fall back to it; do not conclude "it didn't happen" from an empty `session_search`.
## Where the data lives
Each profile has its own `state.db` (SQLite). Active profile path pattern:
`~/.hermes/profiles/<profile>/state.db`. The session DB is the SAME file across a
reset — a reset does not start a new DB, it continues in the same `state.db`.
Key tables (verified schema):
- `sessions` — one row per session. Useful cols: `id` (TEXT, e.g.
`20260731_090857_d1cfb9`), `source` (`tui`/`cli`/`discord`/...), `title`,
`started_at` (REAL unix), `ended_at`, `message_count`, `model`, `cwd`,
`profile_name`, `archived`, `pinned`.
- `messages` — one row per message. Useful cols: `id` (INTEGER, monotonic — use for
ordering), `session_id` (TEXT), `role` (`user`/`assistant`/`tool`), `content`,
`tool_calls`, `tool_name`, `timestamp` (REAL), `active`, `compacted`.
- FTS tables `messages_fts*` exist but you usually don't need them — a `LIKE` on
`messages.content` is simpler and catches what FTS missed.
Other state DBs you may want (same per-profile / global layout):
- cron runs: `<profile>/cron/executions.db`
- kanban: `kanban/boards/main/kanban.db`
- memory (mnemosyne): `<profile>/mnemosyne/data/mnemosyne.db`
- response store / verification: `<profile>/response_store.db`, `verification_evidence.db`
## Recipes (run via terminal + sqlite3)
Set `DB=~/.hermes/profiles/<profile>/state.db` first. Use `-separator` for readability.
1) Recent sessions by source:
```
sqlite3 -separator ' | ' "$DB" "SELECT id, source,
datetime(started_at,'unixepoch','localtime'), title, message_count
FROM sessions WHERE source='tui' ORDER BY started_at DESC LIMIT 8;"
```
2) Find sessions containing a keyword (join to messages):
```
sqlite3 -separator ' | ' "$DB" "SELECT s.id, s.title,
datetime(min(m.timestamp),'unixepoch','localtime') AS first, COUNT(*) n
FROM messages m JOIN sessions s ON s.id=m.session_id
WHERE m.content LIKE '%KEYWORD%' AND m.role IN ('user','assistant')
GROUP BY s.id ORDER BY first DESC LIMIT 6;"
```
3) Read the tail (or head) of one session:
```
sqlite3 -separator $'\n\n===MSG===\n\n' "$DB" "SELECT role||' [id='||id||']: '||
substr(content,1,2500) FROM messages WHERE session_id='SID'
AND role IN ('user','assistant') AND content IS NOT NULL AND content!=''
ORDER BY id DESC LIMIT 8;"
```
(Flip to `ORDER BY id ASC` for the head. `substr` keeps huge tool outputs out of view.)
A ready-made probe is in `scripts/session_probe.sh` — pass profile + keyword.
## GOTCHAS (the part that actually costs time)
1. **Session-ID reuse after a reset.** When a session is `/reset`, the new session
frequently INHERITS the old session's `id`. So `WHERE session_id='SID'` returns
BOTH the pre-reset and post-reset messages mixed together — and your OWN current
queries/messages may share that id too. To read only the pre-reset content, anchor
on a known message id (e.g. the reset marker or the first new user message) and
filter `AND id < ANCHOR_ID`. Find the anchor first:
`SELECT id FROM messages WHERE session_id='SID' AND content LIKE '%<reset cue>%' LIMIT 1;`
2. **Order by message `id`, not `timestamp`.** `id` is monotonic within the DB and is
the reliable within-session order. `timestamp < X` filters can behave unexpectedly
(clock skew, REAL precision); when a time cutoff misbehaves, switch to an `id` cutoff.
3. **`session_search` lag / misses.** Discovery can return 0 rows for sessions that
ARE present (FTS not yet committed, or just-reset sessions). Empty result ≠ absent.
Confirm against the raw DB before telling the user something wasn't found.
4. **Your own session pollutes self-referential queries.** If you search for text you
just typed this session, you'll match your own messages. Restrict by `id < ANCHOR`
or exclude the current session window when hunting for PRIOR-session content.
## Pitfalls
- Don't `cat`/dump whole tables — they're large. Always `LIMIT`, `substr(content,...)`,
and filter `role IN ('user','assistant')` (tool rows are noisy and huge).
- Don't write to these DBs. Read-only inspection; Hermes owns the write path.
- Don't conclude a session is gone from an empty `session_search` — verify in `state.db`.
- This skill is READ forensics. For configuring/using Hermes broadly, the bundled
`hermes-agent` skill is the reference; this one only covers direct-DB recovery.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!