Skip to content

Data directory

The data directory is everything RunWisp persists. SQLite database, captured task output, the daemon’s password, the PID file. It’s set by the --data flag (default data/, relative to the working directory at startup).

There is no RUNWISP_DATA_DIR environment variable. The location is controlled exclusively by --data. If the directory doesn’t exist, the daemon creates it (and any missing parents) with 0700 permissions on first boot. If creation fails, the daemon refuses to start.

data/
├── runwisp.db # SQLite database (run history, config rows)
├── runwisp.db-shm # SQLite WAL shared-memory file
├── runwisp.db-wal # SQLite WAL log
├── password # 22-char base62 password, plaintext, 0600
├── daemon.pid # current PID, plaintext, 0600
├── daemon.log # only when self-spawned via the default TUI path
└── logs/
└── <task-name>/
├── 20260507_143022_a1b2.log
├── 20260507_143022_a1b2.log.idx
├── 20260507_143022_a1b2.log.tidx
└── 20260507_143022_a1b2.log.meta
PathFormatPermsCreated
runwisp.dbSQLite v3 + WAL0644First boot.
runwisp.db-shm, -walSQLite WAL sidecars0644First write to the database.
passwordplaintext + newline0600First boot if RUNWISP_PASSWORD unset; rewritten when env var changes.
daemon.piddecimal PID + newline0600Daemon startup; removed on graceful shutdown.
daemon.logplaintext (stderr)0600Only when runwisp (no args) self-spawns a background daemon.
logs/<task>/directory tree0700First run of that task.
*.log, *.log.idx, *.log.tidx, *.log.meta(see below)0644Per-run, on each task firing.

The daemon does not rely on the process umask — every file and directory creation passes an explicit mode. Inheriting a permissive umask from the parent shell won’t loosen these.

Each run writes one .log file plus three sidecars under logs/<sanitized-task-name>/:

FileFormatUsed for
*.logUTF-8 text, stream-prefixedThe actual captured output.
*.log.idxBinary: 8-byte little-endian uint64 offsets, one every 1024 linesFast seek-by-line for scrubbing huge logs.
*.log.tidxBinary: 12-byte records (uint32 line + int64 ms) every 1024 lines or 1 secondFast time-range queries in the Web UI.
*.log.metaJSONRotation bookkeeping when log_on_full = "drop_old".

The naming convention is:

{YYYYMMDD}_{HHMMSS}_{run-id-suffix}.log

run-id-suffix is the last four characters of the run’s ULID. Sorted alphabetically, files are also sorted chronologically.

Lines inside the .log file carry a stream prefix:

PrefixStream
(none)stdout
[ERR] stderr
[SYSTEM] system messages from the daemon (e.g., disk-pressure log-stop notice)

When a run rotates because it hit log_max_size with drop_old, the old file becomes *.log.prev (with *.log.idx.prev and *.log.tidx.prev companions) and *.log.meta records the cumulative rotated line / byte counts so the Web UI presents a continuous run-line numbering.

A few things you might expect to find in the data dir live in SQLite instead, in the config_entries key-value table:

Row keyWhat it is
jwt_secret32-byte HMAC key for JWT signing. Generated on first boot.
password_hashHash of RUNWISP_PASSWORD (only if it’s currently set via env). Used to detect a deliberate password change → JWT rotation.
fingerprintThe daemon’s instance fingerprint. Computed from machine-id + cwd, then cached.

Treat runwisp.db itself as sensitive: if it leaks, so does the session-signing key.

  • runwisp.toml — the user’s config lives wherever --config points (default: runwisp.toml in the working directory). The separation is deliberate: edit the config in your editor, version it in git, deploy it via your usual sync mechanism.
  • TLS certificates and keys — RunWisp serves plain HTTP on 127.0.0.1 and expects a reverse proxy to terminate TLS. Cert storage is your proxy’s problem.
  • Notification templates referenced by template_path — those live wherever you put them; the daemon reads from the absolute path you configured.

A consistent snapshot of a running daemon needs all three SQLite files together. Copy them at the same moment:

Terminal window
cp -a data/runwisp.db{,-shm,-wal} /backups/runwisp-$(date +%F)/
cp -a data/logs /backups/runwisp-$(date +%F)/logs/

If you only copy runwisp.db you may end up with a stale view — recent writes still in the WAL won’t be there. The official SQLite docs on backups cover this in detail.

A few things to know:

  • The daemon is happy to keep writing while you copy. SQLite WAL mode permits concurrent readers.
  • For atomicity, prefer the SQLite .backup API or sqlite3 .dump over cp if your filesystem doesn’t snapshot atomically.
  • Logs are append-only (until rotation) — copying them while a task is running is safe but you may capture a half-line at the tail.
  • data/password — plaintext credential.
  • data/runwisp.db* — contains the JWT secret and the password hash.
  • data/daemon.pid, data/daemon.log — process state, not source.

If you keep the data dir alongside your config in a repo, add a .gitignore:

/data/

On a fresh data directory, the daemon walks through this in order:

  1. Ensure the directory exists. os.MkdirAll(--data, 0700).
  2. Open the database. Creates runwisp.db, runs the schema, sets journal_mode = WAL and busy_timeout = 5000.
  3. Resolve the fingerprint. RUNWISP_FINGERPRINT env → DB row → compute from machine-id + cwd, then store.
  4. Resolve the password. RUNWISP_PASSWORD env → data/password file → generate fresh + write.
  5. Resolve the JWT secret. config_entries.jwt_secret → generate fresh + store. Rotate if the env-supplied password changed since last boot.
  6. Reconcile last shutdown. Any rows still marked running are marked crashed with exit code -2. (See crash safety.)
  7. Write daemon.pid. With the current PID, 0600.
  8. Bind the HTTP listener on --host:--port.

If any step fails, the daemon exits 1 before opening the port — you won’t see a half-running daemon stuck somewhere in step 6.

Killing the daemon (SIGKILL, power loss, OOM) won’t corrupt the database — SQLite WAL mode is the durability story. On restart, the reconciliation step (above) marks any in-flight runs as crashed with exit code -2. They are not resumed — that’s deliberate, see How scheduling works for why “auto-resume” would be unsafe.

There are no temp files, lock files, or sockets that need cleaning up. The only cleanup is the stale daemon.pid after a hard crash; the next daemon overwrites it on startup.