{"slug": "an-append-only-hash-chained-audit-log-in-sqlite-and-a-script-to-check-it", "title": "An append-only, hash-chained audit log in SQLite, and a script to check it", "summary": "Nicholas Flemmer built Styx, an Apache-2.0 guardrail for coding agents that records every access request, grant, use, denial, revocation and expiry in an append-only, hash-chained audit log stored in local SQLite. Each row carries a sequence number, the previous row's hash and a SHA-256 hash of the prior hash plus the row's contents, with triggers blocking updates and deletes and secrets scrubbed before writing. Flemmer also published a short Node script that exports the table read-only via sqlite3 and verifies the chain, while noting the log cannot stop someone with file write access from rewriting and rehashing the entire history.", "body_md": "When a coding agent can ask for production access, the question afterwards is simple: who touched production, with what, and why? [Styx](https://github.com/NicholasFlemmer/styx-app) answers it with an audit log on your machine. This post is about how that log is built, what it does and doesn't protect against, and a short script you can run to verify it.\n\nEvery time an agent asks for access, and every time access is granted, used, denied, revoked or expires, one row is written. Connecting or removing a target, changing a policy and opening a pull request are rows too. Each row carries:\n\n`$ vercel deploy --prod`\nBefore a row is written, anything shaped like a secret is scrubbed out of commands, reasons and labels, so a token an agent pasted into a command line doesn't end up in the log.\n\nThe log is a table in the local SQLite database. Triggers on the table abort any update or delete. Nothing in the app edits a row, and rows are only written through one function, `AuditService.append`. When a grant is revoked, Styx doesn't change the 'granted' row; it adds a 'revoked' row.\n\nEach row has a sequence number, the hash of the row before it (`prev_hash`) and its own `hash`: a SHA-256 of the previous hash and the row's contents. Change any field of any row, or remove one from the middle, and the chain breaks from that point on.\n\nThe app has no verify button yet, but the data is just a table. Export it read-only with `sqlite3` (3.33 or later, for `-json`) and pipe it into Node:\n\n```\nsqlite3 -readonly -json ~/Library/Application\\ Support/Styx/styx.db \\\n  \"SELECT * FROM audit_entries ORDER BY seq\" | node verify-audit.mjs\n```\n\nWith this saved as `verify-audit.mjs`:\n\n``` js\nimport { createHash } from 'node:crypto';\n\nconst chunks = [];\nfor await (const chunk of process.stdin) chunks.push(chunk);\nconst rows = JSON.parse(Buffer.concat(chunks).toString('utf8') || '[]');\nconst camel = (k) => k.replace(/_([a-z])/g, (_, c) => c.toUpperCase());\n\nlet prev = '';\nfor (const r of rows) {\n  const row = {};\n  for (const [k, v] of Object.entries(r)) if (k !== 'hash') row[camel(k)] = v;\n  const canonical = JSON.stringify(row, Object.keys(row).sort());\n  const hash = createHash('sha256').update(prev).update('\\n').update(canonical).digest('hex');\n  if (row.prevHash !== prev || hash !== r.hash) {\n    console.log(`Chain broken at entry ${row.seq}`);\n    process.exit(1);\n  }\n  prev = r.hash;\n}\nconsole.log(`Chain intact: ${rows.length} entries`);\n```\n\nIt prints `Chain intact` with the number of entries, or the first row where the chain breaks. The database is at `~/Library/Application Support/Styx/styx.db` on a Mac, `%APPDATA%\\Styx\\styx.db` on Windows and `~/.config/Styx/styx.db` on Linux.\n\nThe chain shows that rows weren't edited one at a time after the fact. It doesn't stop someone who can write to your files from rewriting the whole log and recomputing every hash. If that matters to you, copy the latest `hash` somewhere else now and then; a later rewrite of the history before it won't match.\n\nIt's also a record, not a barrier. Styx is a guardrail, not a sandbox: agents run as you, and one that deliberately goes around Styx's tools isn't stopped by the log or by anything else in it. What the log gives you is a trustworthy account of everything that did go through Styx.\n\nThe code is Apache-2.0 at [github.com/NicholasFlemmer/styx-app](https://github.com/NicholasFlemmer/styx-app).", "url": "https://wpnews.pro/news/an-append-only-hash-chained-audit-log-in-sqlite-and-a-script-to-check-it", "canonical_source": "https://dev.to/nicholasflemmer/an-append-only-hash-chained-audit-log-in-sqlite-and-a-script-to-check-it-he9", "published_at": "2026-10-22 15:02:26+00:00", "updated_at": "2026-10-22 15:14:05.998282+00:00", "lang": "en", "topics": ["ai-agents", "ai-safety", "developer-tools", "ai-tools"], "entities": ["Styx", "Nicholas Flemmer", "SQLite", "Node.js", "GitHub"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/an-append-only-hash-chained-audit-log-in-sqlite-and-a-script-to-check-it", "markdown": "https://wpnews.pro/news/an-append-only-hash-chained-audit-log-in-sqlite-and-a-script-to-check-it.md", "text": "https://wpnews.pro/news/an-append-only-hash-chained-audit-log-in-sqlite-and-a-script-to-check-it.txt", "jsonld": "https://wpnews.pro/news/an-append-only-hash-chained-audit-log-in-sqlite-and-a-script-to-check-it.jsonld"}}