# Hermes State Internals

> Inspect/edit Hermes state.db & projects.db safely.

- Skill: `wcpaka-lgtm/hermes-state-internals` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add wcpaka-lgtm/hermes-state-internals`
- Raw SKILL.md: https://api.skillmd.com/api/skills/wcpaka-lgtm/hermes-state-internals/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: MIT
- Author: wcpaka-lgtm (https://skillmd.com/u/wcpaka-lgtm)
- Updated: 2026-09-21
- Page: https://skillmd.com/skills/wcpaka-lgtm/hermes-state-internals

---


# Hermes State Internals

How to inspect and safely modify Hermes' on-disk SQLite state stores — when the
GUI/sidebar shows something confusing ("my sessions disappeared", "which project
owns this session", "where does X live") and the answer is in the databases, not
a config flag.

This is the **state-layer** companion to `inspecting-hermes-desktop-dom` (live
DOM/CDP) and `debugging-hermes-tui-commands` (TUI code). Use those for runtime
UI; use this for the persisted data underneath.

## Where the stores live

Resolve the real home from `$HERMES_HOME` (never hardcode `~/.hermes`; on Windows
it's `%LOCALAPPDATA%\hermes`, e.g. `C:\Users\<user>\AppData\Local\hermes`).

| File | Holds |
|------|-------|
| `state.db` | Canonical session store: `sessions`, `messages` (+ FTS), model usage, routing |
| `projects.db` | Desktop Projects (named workspaces): `projects` table |
| `kanban.db` | Kanban boards/tasks |
| `memories/` | Memory + user-profile stores (separate dir, not in state.db) |

## Hard rules before ANY write

1. **Back up first**: `cp state.db state.db.pre-<change>-$(date +%Y%m%d_%H%M%S).bak`
2. **Inspect schema before assuming columns** — `PRAGMA table_info(<table>)`. Schemas drift across versions; never trust a remembered column list.
3. **The desktop app caches the DB** — after a direct edit it may not reflect until the sidebar refreshes or the app restarts. Tell the user this.
4. Prefer `hermes` CLI / tools over hand-editing when a supported path exists. Direct SQL is the fallback when no CLI covers it.

## The session→project mapping (the big one)

The desktop sidebar decides which Project a session belongs to **by the session's
`cwd` column** (falling back to `git_repo_root`): a session is filed under the
explicit project whose folder is the longest path-prefix of its `cwd`. Sessions
with `cwd = NULL` (or a cwd under no project folder) fall into the synthetic
**"Home" bucket** (`__no_project__`) and look "missing" from a project view —
they are NOT deleted, just unfiled.

Matching logic lives in `apps/desktop/src/app/chat/sidebar/projects/workspace-groups.ts`
(frontend overlay) and `tui_gateway/project_tree.py` (authoritative backend tree).
Windows path comparison is case- and separator-insensitive.

**Practical consequence:** creating a Project (`project_create`) does NOT
retroactively claim old sessions — they keep their old/NULL cwd. To move existing
sessions into a project, update their `cwd` to a path under the project folder.

See `references/state-db-recipes.md` for the exact schemas and the tested
session-migration SQL.

## Pitfalls

- `sessions` has **no** `last_active` column — derive it via `MAX(messages.timestamp)` per session. `messages` uses `timestamp`, not `created_at`.
- Don't conflate the desktop **Files pane** (right sidebar, shows the active project's folder tree) with the **session list** (left sidebar). "I see two files on the right" = Files pane; "my chats are gone" = session list / project filtering.
- `project_list` returning empty `projects: []` while the sidebar shows a project = you're querying a different profile/store than the desktop app, or the app hasn't synced. Check you're reading the same `projects.db`.

