Two state.db errors come up often enough that they deserve a single article. The first is sqlite3.DatabaseError: database disk image is malformed, which means the SQLite file got corrupted. The second is PermissionError: [Errno 13] Permission denied: '/var/hermes/state.db', which means the user Hermes runs as can't write to the database location.
Both are recoverable. Different fixes for what look like similar errors. This guide covers both.
Why state.db gets corrupted
SQLite is robust on its own. What corrupts it is hostile shutdowns. The four common causes:
1. WSL2 distros killed hard by Windows
Windows sends SIGKILL when you close the last terminal. Any in-flight write to state.db is left half-finished. Next start, SQLite refuses to read the inconsistent page.
2. Docker containers killed not stopped
docker kill is SIGKILL. docker stop is SIGTERM with grace period. Same outcome as the WSL2 case if you used kill.
3. Two Hermes processes writing concurrently
Someone has CLI Hermes open. Starts a Desktop instance pointing at local mode. Or the gateway is running under systemd and the user runs a CLI command thinking it's read-only. Both processes try to write. SQLite locks fail. Pages get corrupted.
4. Disk-full events
SQLite stops mid-write because the kernel can't allocate more space. Partial transaction left on disk.
Step 1: Integrity check first
Before doing anything else, find out how bad the damage is:
sqlite3 ~/.hermes/state.db "PRAGMA integrity_check;"
If you see ok, the file isn't corrupted and your error came from somewhere else. Possibly a code-level bug, not a data-level one.
If you see a list of errors, the file is genuinely damaged. Move to step 2.
Faster but less thorough alternative
sqlite3 ~/.hermes/state.db "PRAGMA quick_check;"
Step 2: Repair using SQLite .recover
SQLite's .recover dumps every readable row out of a damaged file. You then reconstruct a fresh database from the dump.
Manual recover
cp ~/.hermes/state.db ~/.hermes/state.db.broken
cd ~/.hermes
sqlite3 state.db.broken ".recover" > recovered.sql
sqlite3 state.db.fresh < recovered.sql
mv state.db state.db.original
mv state.db.fresh state.db
sqlite3 ~/.hermes/state.db "PRAGMA integrity_check;"
If state.db.fresh comes back ok on integrity_check, you recovered. Severely damaged files lose a few rows in the damaged pages; usually that's old session history rather than recent stuff.
Hermes wraps this for you
hermes memory repair
Same approach with sensible defaults. Use this first. Fall back to the manual SQL if hermes memory repair fails on a specific section.
Step 3: Nuclear option (last resort)
If recovery doesn't work at all, you can reset state.db. You lose session history. Skills, persona and the memory markdown files survive.
cp ~/.hermes/state.db ~/.hermes/state.db.backup
hermes memory reset --confirm
Only do this if recovery genuinely failed. Session history is useful and once gone it's gone. Our Hermes backups guide covers backing up state.db regularly so you can restore from yesterday's good copy rather than wipe.
Now the permission error
Different problem, similar-looking error. The file is fine, you just can't write to it.
Check ownership
ls -la ~/.hermes/state.db
id
stat ~/.hermes/state.db
Fix ownership
If state.db is owned by root and you're running Hermes as your normal user:
sudo chown -R $USER:$USER ~/.hermes
This happens when someone runs Hermes as root once for testing. The database gets created with root ownership. Subsequent non-root runs fail.
If state.db is in /var/hermes
Either change the ownership of the system directory, or move state.db back to ~/.hermes where it belongs. Keeping state.db in the user's home dir and running the agent as that user avoids most permission issues.
The Docker UID mismatch case
Subtle permission issue. Container runs as a user with UID 1000 baked into the image. Your host user might also be UID 1000 (everything works) or might be UID 1001 (container can't write to mounted volume).
id
ls -la ~/.hermes/state.db
docker compose exec hermes id
If host UID and container UID don't match: either chown host directory to container UID, or rebuild container with host UID via build args.
Full compose pattern in our Hermes Docker Compose tutorial.
Preventing future corruption
On WSL2
Use wsl --shutdown properly rather than just closing the last terminal. Run Hermes under systemd inside WSL with proper Restart and ExecStop directives so the gateway has time to close cleanly.
On Docker
Use docker compose stop not docker compose kill. Configure your service with stop_grace_period: 30s in compose so SQLite finalises writes during shutdown.
Everywhere
Never run two Hermes processes against the same state.db. Lock-related errors documented in our database is locked error guide.
Back up state.db nightly. SQLite backup API survives corruption you didn't notice yet. Pattern in our Hermes backups guide.
Missing assistant messages variant
One state.db quirk worth knowing: pre-v0.14 Hermes had a bug where assistant messages got silently dropped from state.db. User and tool messages persisted, some assistant replies didn't. Symptoms: "the agent doesn't remember what it said yesterday" or session search returns user prompts but no replies.
hermes upgrade
hermes --version
Fixed in later versions. Full issue at the Hermes issue tracker.
What I do to prevent this on my own boxes
Two habits.
- Agent runs under systemd with a long graceful shutdown timeout. SQLite always closes cleanly.
- Nightly cron uses the SQLite backup API to copy state.db to a separate disk.
Both habits prevent the failure modes above.
LumaDock template defaults
The Hermes Agent template on LumaDock has the systemd unit pre-configured with the right shutdown semantics. The backup script is on the box waiting to be enabled. Unmetered bandwidth and no setup fees on every plan. Setup details in our Hermes Agent complete guide.
One sanity habit worth keeping
If integrity_check ever returns anything other than ok, even once, copy state.db before trying to fix it. The recovery flow only works if you still have the original to recover from. A bad fix attempt on the only copy is the worst-case version of this problem.

