#State directory
nyx-agent keeps every artifact that has to outlive a process under
a single root directory: the SQLite store, repo workspaces, repro
bundles, structured logs, and the local bearer token the API uses
to authenticate the embedded SPA. Resetting Nyx Agent on a host means
removing that one directory.
#Where it lives
On first launch the agent resolves the root in this order:
--state-dir <PATH>on the command line.[general] state_dirinnyx-agent.toml.dirs::data_dir()/nyx-agent. On macOS that's~/Library/Application Support/nyx-agent; on Linux,$XDG_DATA_HOME/nyx-agent(defaulting to~/.local/share/nyx-agent).
nyx-agent doctor prints the resolved path:
state dir OK at /Users/eli/Library/Application Support/nyx-agent
If dirs::data_dir() cannot resolve a base (no HOME, no
XDG_DATA_HOME), startup fails with could not resolve user data directory (HOME/XDG_DATA_HOME unset?). Pass --state-dir to work
around it.
#Layout
<state>/
├── state.db SQLite store. Schema managed by the migrator.
├── state.db-wal WAL journal. Present while the daemon holds the pool open.
├── state.db-shm Shared-memory file for WAL readers.
├── auth_token Local bearer token (mode 0600). Minted on first serve.
├── logs/
│ └── agent.jsonl One JSON event per line, written by the tracing JSON layer.
├── repos/ Per-repo ingestion workspace, legacy flat layout.
│ └── <repo-name>/
├── projects/
│ └── <project-id>/
│ └── repos/
│ └── <repo-name>/ Project-scoped workspace. New ingestions write here.
├── bundles/ Repro bundles. One tarball per finding when the operator builds one.
├── traces/ AI conversation logs and live-verification evidence.
├── cache/ Reserved.
├── findings/ Reserved.
└── runs/ Reserved.
The root and every subdirectory in that list are created together
by StateDir::ensure on the first nyx-agent invocation; later
invocations are idempotent.
#Permissions
On Unix every directory in the tree is forced to mode 0700 on
every ensure() call, so a second user account on the same host
cannot read the agent's state even if the parent permissions are
loose. auth_token is written with mode 0600. On non-Unix
platforms the permission step is a no-op.
If ensure cannot set permissions (read-only filesystem, ACL
conflict) the daemon fails to start with failed to set permissions on <path>: <io error>.
#Files
#state.db
SQLite database. The pool opens with WAL journaling, synchronous = NORMAL, foreign_keys = ON, cache_size = -8000 (8 MiB), and
temp_store = MEMORY. The schema is managed by bundled SQLx
migrations under crates/nyx-agent-core/migrations/:
| Migration | Adds |
|---|---|
0001_v1.sql |
Full baseline schema: product/projects, repos, runs, findings, harness specs, traces, AI budgets, quarantine data, launch profiles, Nyx signals, pentest candidates, verification attempts, vulnerabilities, attack graph, project integrations, business-logic template summaries, phase events, and supporting indexes. |
The singleton meta row carries schema_version (mirrors
MAX(_sqlx_migrations.version)), created_at (epoch ms of first
install; never overwritten), and agent_version (the binary that
opened the file most recently). Read it back with:
sqlite3 "<state>/state.db" \
"SELECT schema_version, agent_version, datetime(created_at/1000, 'unixepoch') FROM meta WHERE id = 1;"
nyx-agent doctor prints the schema version on every run:
db OK at <state>/state.db (schema v1)
If migrations diverge (e.g. a newer binary then an older binary
opens the file), the older binary refuses to start with failed to apply migrations: .... Reverting to a newer binary fixes it; there
is no down-migration story.
Reset the database without touching other artifacts:
nyx-agent reset db
That command removes state.db, state.db-wal, and state.db-shm
after checking that no running nyx-agent process has the database open.
Use nyx-agent reset db --yes in scripts.
#auth_token
64 hex characters (32 random bytes). The HTTP + WebSocket server
reads this on serve startup and requires every API client to
present it as Authorization: Bearer <token>. Loopback-only
binding makes this an additional defence in depth, not the primary
control.
--headless skips minting the token; the SPA is not served in that
mode, so no bearer is needed.
Rotate by deleting the file: serve mints a fresh one on the next
start. Live sessions invalidate at that point.
#logs/agent.jsonl
Newline-delimited JSON, one record per tracing event. The format
matches tracing-subscriber's fmt::json layer: timestamp,
level, target, fields, and span fields the instrumented
function attached (run_id, task_id, ...). Operators tail this
file with jq:
tail -f "<state>/logs/agent.jsonl" | jq -c 'select(.level=="ERROR")'
The stderr layer mirrors the same events at the level set by
--log-level / [general] log_level; the JSON layer is always at
full verbosity so the file is the canonical record.
#traces/<run-id>/browser_verification/<attempt-id>/
Browser-driven verification writes durable replay evidence here when a
candidate is exercised through Playwright. The verification attempt row
stores the file paths in verification_attempts.artifact_paths_json.
Typical files are:
browser-replay.jsonandbrowser-replay.mjs: deterministic, redacted replay inputs.browser-final.pngplus any explicit screenshot-step captures.browser-dom.htmlandbrowser-focused-html.json: redacted DOM evidence.browser-console.jsonandbrowser-timeline.json: console output and action/navigation history.playwright-trace.zipwhen trace capture is available and safe, orplaywright-trace-unavailable.jsonwhen the executor falls back to the redacted replay artifacts.
Session headers, cookies, bearer tokens, and token-like query/body values are redacted before Nyx Agent writes JSON, HTML, console, timeline, or replay files. Playwright traces are skipped when the plan or injected session headers contain secret-like values because the zip is not post-process-redacted.
#Directories
#repos/<name>/
Legacy flat layout. New ingestion calls land under projects/<id>/repos/<name>/
instead; the flat directory remains for repos created before the
project entity rolled out.
#projects/<project-id>/repos/<repo-name>/
Per-repo ingestion workspace. For a git-sourced repo this is a
shallow clone, refreshed via git fetch on subsequent runs. For a
local-path source this is a read-only snapshot rebuilt per run
and removed at end of run so concurrent IDE edits never race the
scan. The workspace path is recorded on the IngestedRepo returned
to the dispatcher and never assumed by anything outside the
ingestion call.
Two repos with the same name in different projects do not collide.
#bundles/
Repro bundles, one tarball per finding when an operator requests
one. The path is recorded in repro_bundles.path so the API can
serve it back over GET /api/v1/findings/:id/repro after verifying
the on-disk path stays under this root.
#cache/, findings/, runs/
Created on ensure() for parity with the spec but no shipping code
writes here yet. Reserved.
#Override examples
Run against a tempdir for a one-off scan:
nyx-agent --state-dir /tmp/nyx-agent-scratch doctor
nyx-agent --state-dir /tmp/nyx-agent-scratch scan --project demo
Pin the directory in nyx-agent.toml (handy for systemd /
launchd units that set their own HOME):
[general]
state_dir = "/var/lib/nyx-agent"
Either form works for any subcommand. --state-dir wins when both
are set.
#Failure modes
| Symptom | Cause / Fix |
|---|---|
could not resolve user data directory (HOME/XDG_DATA_HOME unset?) |
No default base. Pass --state-dir or set [general] state_dir. |
failed to create <path>: <io> |
Parent unwritable, ENOSPC, or filesystem refuses 0700. Pick a writable root. |
failed to set permissions on <path>: <io> |
The filesystem cannot honour 0700 (e.g. NFS with noexec/ACLs). Move the state dir to a local FS, or override on Windows where the step is a no-op. |
failed to apply migrations: ... |
Older binary opening a newer database. Re-launch with the version that wrote the file. |
failed to open database at <path>: ... |
state.db exists but is not a valid SQLite file, or the directory is read-only. Inspect with sqlite3 <path>. |
API returns 401 unauthorized after deleting auth_token |
Token was minted but the SPA still holds the previous value. Refresh the page. |
#Related pages
- Install for the runtime prerequisites that decide
where
dirs::data_dir()resolves. - Configuration for
[general] state_dirand every other section. - CLI for
--state-dir,--config, and--log-level.