Search

    MENU

    Data and backups

    Markdown

    Zinn stores every project in one local SQLite database. The default database is ~/.zinn/data/zinn.sqlite. The backups of the database are stored in ~/.zinn/backups/, failure logs in ~/.zinn/logs/, and local telemetry records in ~/.zinn/telemetry/.

    ZINN_DIR or a settings file can select another Zinn directory. See Settings for directory selection and precedence.

    Reported database paths, and the paths of backups Zinn saves, are absolute with existing symbolic links resolved. For a destination that does not exist yet, Zinn resolves its nearest existing ancestor and appends the missing path segments without creating them. Migration reports the same resolved destination whether it upgrades the database or finds it current. A path-resolution failure identifies the requested path.

    The CLI and core library can share the selected database. On a local filesystem, commands can share it too: Zinn waits up to 30 seconds for each lock acquisition, then fails if the lock is still held. Of course, the command’s total running time can exceed 30 seconds.

    Back up the database

    Save a snapshot before installing a new version:

    			
    				
    						
    						zinn backup
    					
    			
    		

    The snapshot includes committed changes and can be taken while Zinn is in use. It is saved to backups/ in the selected Zinn directory, and an existing backup is never overwritten. With --json, the command returns { "path": "..." }. It fails when there is no database yet.

    On macOS and Linux, saving a backup syncs its directory and parent directories as well as the file. A sync failure stops the operation. An upgrade or restore that needs the backup leaves the database unchanged, and a completed backup file is retained.

    Use zinn backup for a complete copy: committed changes may still be in data/zinn.sqlite-wal, so copying data/zinn.sqlite alone can omit them. To move the database to another machine, copy the generated snapshot and run zinn restore <path> there.

    Restore a database

    Restore the whole database from a backup saved by zinn backup or an upgrade:

    			
    				
    						
    						zinn restore ~/.zinn/backups/2026-09-29T12-30-05Z-cli-0.32.0-b38178c0.sqlite
    					
    			
    		

    Restore asks for confirmation unless --yes is used. It saves the current database first, then applies the restore completely or not at all. Restoring to a missing database creates it. With --json, it returns databasePath, previousVersion, restoredVersion, and backupPath; backupPath is null if the destination database was missing or empty. See Scripting and JSON output for confirmation behavior in unattended commands.

    A backup with an older schema version is restored at that version, then Zinn upgrades it under the normal upgrade policy. Restore works regardless of the version of the destination database, so you can return to an older Zinn and restore a backup saved by an upgrade. Missing, damaged, unmarked, non-Zinn, or newer-schema backups are refused without changing the database. So is a backup path that leads to the database itself, and a backup file that is deleted, replaced or overwritten after restore checked it. Any later failure rolls back the restore and retains the saved backup. If the destination database is damaged, restore leaves it unchanged and prints recovery steps.

    Deleted projects and tasks

    Deleting a task removes it from its column. Deleting a project removes its columns and tasks. Their recorded activity remains in the database, and restoring a backup taken before the deletion brings the deleted records back.

    Task key reuse

    Deleting a task does not make its number available again within that project. Deleting a project releases its key. A project created later with the same key starts task numbering again at 1, so a task key such as MDR-1 can name a different task over time. The earlier task and its activity remain in history under their own IDs.

    Upgrade a database

    A newer Zinn upgrades an older database completely or not at all, saving a backup first. A published release upgrades an older database on first use. A source build upgrades only a separate database selected with ZINN_DIR; other older databases open read-only until explicitly migrated.

    To upgrade explicitly, run:

    			
    				
    						
    						zinn migrate
    					
    			
    		

    An up to date database is unchanged when a migration is attempted, which means no backup is saved for it either. With --json, the result includes databasePath, previousVersion, finalVersion, changed, and backupPath. When nothing is supposed to happen on a migrate call (like the aforementioned case), backupPath is null. An upgrade failure leaves the database unchanged and keeps backups from each attempt. zinn migrate fails without attempting an upgrade when no database with data exists, when the database has no schema version, or when it is newer than this Zinn.

    A database with a newer schema can be read, but Zinn refuses changes to it. Use a newer Zinn to change it.

    Failure logs

    A CLI command that fails with a storage or internal error writes one log file to logs/ in the selected Zinn directory and names the file in its error. Other errors and successful commands write no log. A log’s name follows the same pattern as a backup’s, such as 2026-09-29T12-30-05Z-cli-0.32.0-b38178c0.log.

    A log holds the stack trace with its causes, the process id, the Zinn and runtime versions, the operating system, the store and database paths, the schema version, the arguments as typed, timings, and the notices printed before the failure. With ZINN_DEBUG=1, the notices include the debug output. The text is for humanoids, and its layout can change.

    Arguments keep their values, so a log can contain private work such as a task’s title or description. Read a log before sharing it.

    Writing a log keeps the newest 10 logs and removes older ones. Other files in logs/ stay. Zinn writes logs only inside an existing Zinn directory. When a log cannot be written, the command reports its error without a path.

    Telemetry

    Zinn keeps local usage records. They are on by default, but nothing leaves your machine. Each recorded run appends one line of JSON to a record file in telemetry/ in the selected Zinn directory. A record file is named YYYY-MM-DD-NNNN.jsonl, for the UTC date on which the run started and a sequence number, such as 2026-11-07-0001.jsonl. Zinn creates telemetry/ inside a Zinn directory that exists and never creates the Zinn directory for a record.

    While telemetry is on, Zinn records every run, whether it succeeds or fails, including help requests and a bare zinn. Two kinds of run leave no record:

    • --version or -v as the only argument.
    • A successful whoami, capabilities or info. A failed run of these commands, and a request for their help, is recorded.

    Recording never changes a command’s output or exit code. When a record cannot be written, Zinn skips it, and the ZINN_DEBUG=1 output names the problem on a line starting with ::telemetry.

    Record fields

    A record is one line. Its keys are camelCase, and a fact Zinn does not know is left out instead of setting it as null. Zinn can add fields without changing v.

    • v is the record format, currently 1.
    • startedAt is the start of the run as a UTC time with milliseconds, and durationMs is how long it ran in whole milliseconds.
    • exitCode is 0 or 1.
    • actor holds facts read from the environment, never from git. kind is human or agent. A detected agent adds name and source, such as env:AI_AGENT. sessionId, threadId, agent, model and effort appear when declared, as for activity. client is always present and holds the client chain.
    • command holds the canonical route, such as task view, and alias when an alias was typed, such as task show. help is true only when a help flag was written. When no command matched, route is absent and command holds namespace, the nearest namespace (empty for the root), and unmatchedWord, the first word that matched no command. unmatchedWord is left out when that word starts with a dash.
    • options lists every option written before --, known or not, including --json. Each entry has the option’s name and never its value. valueForm is joined when the value was written after =, next when a string option took the following word, and none for a flag. It is left out when the words alone cannot tell, such as an unknown option written without =. A word such as -later counts as one option named -l. A word gets an entry only when its name has an option’s shape: after its dashes, a letter or digit followed by letters, digits, _ or -.
    • positionals is the number of positional words the command received, including words after --. It is left out when no command matched, when help was requested, and when the arguments could not be parsed.
    • error appears only on a failure. It holds the error code, and when known the reason, the argument, and the suggestedRoute, the command Zinn suggested. See Handle errors and notices. logFileName is the file name of the failure log this run wrote. That log may have been removed since, because Zinn keeps the newest 10.
    • version is the CLI version. runtime holds the name (Node or Bun) and version of the runtime. platform and arch are Node’s process.platform and process.arch.
    • stdinIsTerminal and stdoutIsTerminal say whether each stream was a terminal, and parentPid is the parent process id.

    For example, a failed task list run by an agent looks like this. A record is a single line, shown here over several lines:

    			
    				
    						
    						{
    	"v": 1,
    	"startedAt": "2026-11-07T09:22:10.170Z",
    	"durationMs": 38,
    	"exitCode": 1,
    	"actor": {
    		"kind": "agent",
    		"name": "claude-code",
    		"source": "env:AI_AGENT",
    		"sessionId": "3f2a9c",
    		"model": "claude-sonnet-5-5",
    		"client": "zinn-cli/0.44.0 rex/0.9"
    	},
    	"command": { "route": "task list", "help": false },
    	"options": [
    		{ "name": "--limit", "valueForm": "next" },
    		{ "name": "--json", "valueForm": "none" }
    	],
    	"positionals": 1,
    	"error": {
    		"code": "storage",
    		"reason": "not_database",
    		"logFileName": "2026-11-07T09-22-10Z-cli-0.44.0-321ef00d.log"
    	},
    	"version": "0.44.0",
    	"runtime": { "name": "Node", "version": "24.4.0" },
    	"platform": "darwin",
    	"arch": "arm64",
    	"stdinIsTerminal": false,
    	"stdoutIsTerminal": false,
    	"parentPid": 4242
    }
    					
    			
    		

    Records never hold option values, positional words, task, project or column keys or names, messages, help text, diagnostics, paths, the working directory or the hostname. The one typed word a record can keep is command.unmatchedWord. Option names are kept as typed, and the actor facts are the values the environment declares. A one-word value that starts with a dash can’t be told from a flag, so --sk-abc123 written without a string option before it or -- is recorded as an option name, and as error.argument when Zinn refuses it. A word without an option’s shape, such as a quoted --my search words, gets no options entry, and error.argument keeps only the part before its first space or =.

    Retention

    telemetry.maxAgeDays sets how many days records are kept and defaults to 90. telemetry.maxMegabytes limits the record files’ total size and defaults to 100, with one megabyte being 1,048,576 bytes. See Settings.

    A record file takes records up to a tenth of the size limit, and the next run then starts a new one. A run that starts a new file first deletes the files older than telemetry.maxAgeDays, then the oldest remaining files until the rest total less than the limit. Only that run deletes anything, so the folder can briefly hold older files. A file’s age comes from its name, and only regular files named like 2026-11-07-0001.jsonl are counted or deleted. Other files in the folder stay.

    Clear or turn off records

    Delete a record file or the whole telemetry/ folder at any time. Zinn creates the folder again when it writes the next record. zinn backup copies only the database, and zinn restore replaces only the database, so both leave telemetry/ alone.

    Setting telemetry.mode to off, or DO_NOT_TRACK to 1, stops new records and keeps existing files. See Settings.

    Core library compatibility

    Core has client-specific database operation rules. See Core library compatibility.