# Settings

Without `ZINN_DIR`, Zinn reads the global `~/.zinn/settings.json` and the nearest optional `zinn.local.json` in a codebase. They can choose the data directory, a default project, how long a write waits for a busy database and the telemetry mode. The global file can also set the telemetry limits and configure hooks. With `ZINN_DIR`, Zinn reads only `settings.json` inside that selected directory. Note that `storage.directory` does not cause settings from its target directory to be read.

For the files and paths that make up a store, see [Data and backups](/docs/data).

## Choose the data directory

The first configured location wins:

1. `ZINN_DIR` selects that directory for the operation.
2. `storage.directory` in the nearest `zinn.local.json` selects a directory for the current codebase.
3. `storage.directory` in `~/.zinn/settings.json` selects the global directory.
4. If none is set, Zinn uses `~/.zinn` as its directory.

If a `ZINN_DIR` is provided, Zinn reads only `settings.json` inside that directory. It does not read the home or codebase settings files. While the selected `settings.json` can set `defaultProject`, `ZINN_DIR` itself selects the directory.

Without `ZINN_DIR`, Zinn looks for `zinn.local.json` from the current directory up to the Git root. Outside a Git repository, it checks only the current directory. It reads the global `~/.zinn/settings.json` too, then merges the local file over it field by field. A local setting overrides the same global setting. You can omit fields in the local setting to inherit global values. The local settings file is not meant to be committed, as it can cause unexpected behavior on others' Zinn setups who install the same repo with the tracked local setting.

> **Hooks and telemetry limits are global-only**
>
> A `zinn.local.json` may set values that apply only to commands run in that codebase. Settings that apply across every codebase, or that run commands, are read only from the global settings file. So `hooks`, `telemetry.maxMegabytes` and `telemetry.maxAgeDays` cannot be set locally. A file that sets one stops normal commands with a settings error.

`storage.directory` is relative to the folder containing the file that sets it. A leading `~` or `~/` expands to your home directory. It names the Zinn directory, which contains `data/zinn.sqlite`, `backups/`, `logs/`, and `telemetry/`.

The selected directory holds one database shared by all its projects. Choose a local directory outside network shares and synchronized folders. See [safe storage and backups](/docs/data).

A folder selected through `ZINN_DIR` or `storage.directory` must already exist. Create it with `mkdir -p <path>` before using it. Creating the first project or label creates the database inside an existing empty folder. With no explicit location, it also creates the default `~/.zinn` folder. Until then, other commands fail with code `not_found` and entity `database` and create no database. In an existing folder, the failed run is still recorded in `telemetry/` unless [telemetry is off](#control-local-telemetry). An empty `ZINN_DIR` is refused. Help, version requests, and `zinn info` work with a missing folder.

For example, this `zinn.local.json` selects a shared data directory and its default project:

```json
{
	"$schema": "https://zinn.sh/schemas/settings/v1.json",
	"defaultProject": "MDR",
	"storage": {
		"directory": "~/work/zinn-data"
	}
}
```

The `$schema` field is optional and gives editors a schema for autocomplete and validation. Zinn ignores it. Settings files are JSON without comments. Unknown keys, invalid JSON, and values of the wrong type stop normal commands with an error naming the file. Zinn **does not** fall back to defaults. A settings version changes only when a change would make existing settings files invalid. When `ZINN_DIR` is set, malformed home and codebase settings are not read.

## Choose a default project

`defaultProject` names the project used when a command supports omitting its project key. Project keys are matched case-insensitively. An explicitly typed key takes precedence, then `zinn.local.json`, then the global settings file. With `ZINN_DIR`, only the selected directory's `settings.json` can provide the default.

The default applies to `task create`, `task list`, `task search`, `project view`, `project column list`, `project edit`, and `project delete` when their project key is omitted. A `null` value is not accepted. Without a default, `task list` and `task search` cover every project; the other commands require a key. A configured default that names a missing project causes a command that uses it to fail and identify the key and settings file. `--all-projects` makes `task list` and `task search` cover every project even when a default is configured; it works with `--all` or `--archived` and cannot be combined with a typed project key.

The `defaultProject` setting applies only to commands that support an omitted key. For example, task-key commands use the project in the task key, and column creation, editing, and ordering require an explicit project key. See the [CLI reference](/docs/cli#start-a-project) for command details.

## Wait for a busy database

One command at a time can write to a database. A command that needs to write while another holds the lock waits, then fails with a `storage` error with reason `busy` if the lock is still held. `busyTimeoutMs` sets how long it waits, in milliseconds:

```json
{
	"busyTimeoutMs": 60000
}
```

It is a whole number from `0` to `2147483647` and defaults to `30000`. `0` fails at once instead of waiting. `zinn.local.json` can set it, and the nearest one overrides the global file.

## Run hooks after changes

`hooks` in the global settings file (`~/.zinn/settings.json`, or `settings.json` inside `ZINN_DIR`) lists commands that Zinn runs after each saved change:

```json
{
	"hooks": {
		"notify": { "command": "sh ~/.zinn/hooks/notify.sh", "timeout": 10 }
	}
}
```

Hooks run commands, so they are read only from the global settings file. A `hooks` key in `zinn.local.json` is a settings error like any other invalid setting, and hooks never run from the local settings file. See [Hooks](/docs/hooks) for what a hook receives and how failures are handled.

## Control local telemetry

Zinn records local usage by default and sends nothing off the machine. See [Telemetry](/docs/data#telemetry) for the records and their limits. The `telemetry` key has three fields, and a file can set any of those it is allowed to:

```json
{
	"telemetry": {
		"mode": "off",
		"maxMegabytes": 50,
		"maxAgeDays": 365
	}
}
```

- `mode` is `local` or `off`. `off` writes no new records and keeps existing files. Without a setting, `mode` is `local`, or `off` when the `DO_NOT_TRACK` environment variable is `1`, following [donottrack.sh](https://donottrack.sh/). Only the value `1` counts. A `telemetry.mode` in a settings file overrides `DO_NOT_TRACK`, whichever way it is set.
- `maxMegabytes` is a positive whole number of megabytes, each 1,048,576 bytes. It limits the total size of the record files and defaults to `100`.
- `maxAgeDays` is a positive whole number of days to keep records. It defaults to `90`.

`zinn.local.json` can set `mode`, and the nearest one overrides the global file. A mode applies only to records written by commands run in that codebase. `maxMegabytes` and `maxAgeDays` are read only from the global file, because the records they delete can come from any codebase. Either limit in `zinn.local.json` is a settings error like any other invalid setting. With `ZINN_DIR`, only that directory's `settings.json` is read, and it can set all three fields.

## Inspect the selected store

Use `zinn info` to inspect the selected store and settings without initializing or upgrading the database:

```sh title="Inspect the selected store"
zinn info
zinn info --json
```

The report includes the selected store directory and source, settings files and their state, default project, telemetry mode and folder, database path and schema state, and CLI, core, and runtime versions. Settings states are `found`, `missing`, `error`, or `not-read` when `ZINN_DIR` selects a different settings file. Database state is `absent`, `empty`, `unmarked`, `current`, `older`, `newer`, or `unavailable`.

`zinn info --json` returns one object with stable top-level keys:

- `settingsFiles` lists each inspected settings file with its `path`, `source` (`env`, `global`, or `local`), `state`, and either its parsed `settings` or an `error`. Unread home and codebase files with `ZINN_DIR` have state `not-read` and reason `ZINN_DIR`.
- `storage` contains the selected `directory`, its `source` (`env`, `local`, `global`, or `default`), the `settingsPath` that chose it (or `null` for `env` and `default`), whether it exists, and an `error`.
- `defaultProject` contains its `key`, the `settingsPath` that supplied it, and an `error`; the key and path are `null` when there is no configured default.
- `telemetry` contains the resolved `mode` (`off` or `local`), its `source` (`default`, `DO_NOT_TRACK`, or `settings`), the `settingsPath` of the file that set the mode (`null` unless `source` is `settings`), `maxMegabytes`, `maxAgeDays`, the `directory` of the telemetry folder, whether it `exists`, the `sizeBytes` of the files directly inside it, and an `error`. A file that sets only `maxMegabytes` or `maxAgeDays` does not change `source`. The size is `0` when the folder is missing, because `info` never creates it. A path that is not a folder is reported with `exists` false, `sizeBytes` `null` and an `error`.
- `database` contains the database `path`, its `state`, its `version`, the `latestVersion` supported by this CLI, and an `error`. The version is `0` for `absent`, `empty`, and `unmarked` states.
- `versions` contains `cli`, `core`, and `runtime` (`name` and `version`).

`database.path` resolves existing <span class="group"><span class="group-hover:hidden">symbolic</span><span class="hidden sr-hidden group-hover:inline">absolute</span></span> links and relative paths to an absolute destination. `storage.directory` records the configured directory. If database-path resolution fails, the report keeps the requested database path with state `unavailable` and an `error`.

For example, when settings and the database can be inspected, the report has this shape:

```json
{
	"settingsFiles": [
		{
			"path": "/home/me/.zinn/settings.json",
			"source": "global",
			"state": "found",
			"settings": { "defaultProject": "DEMO" }
		},
		{ "path": "/work/app/zinn.local.json", "source": "local", "state": "missing" }
	],
	"storage": {
		"directory": "/home/me/.zinn",
		"source": "default",
		"settingsPath": null,
		"exists": true,
		"error": null
	},
	"defaultProject": { "key": "DEMO", "settingsPath": "/home/me/.zinn/settings.json", "error": null },
	"telemetry": {
		"mode": "local",
		"source": "default",
		"settingsPath": null,
		"maxMegabytes": 100,
		"maxAgeDays": 90,
		"directory": "/home/me/.zinn/telemetry",
		"exists": true,
		"sizeBytes": 21345,
		"error": null
	},
	"database": {
		"path": "/home/me/.zinn/data/zinn.sqlite",
		"state": "current",
		"version": 2,
		"latestVersion": 2,
		"error": null
	},
	"versions": {
		"cli": "<installed CLI version>",
		"core": "<installed core version>",
		"runtime": { "name": "Node", "version": "<runtime version>" }
	}
}
```

When settings are unusable or a database cannot be inspected, `info` reports the errors and exits with code 0. `unavailable` means settings left the store undecided (`path` and `version` are `null`), or the selected file could not be read as a database (`path` is set, `version` is `null`, and `error` explains why). Every `error` is `null` when there is no problem. A settings error is carried on `storage`, `defaultProject`, `telemetry`, and `database` as well as its `settingsFiles` entry, so the same string can appear several times. The fields a settings file could have set are `null`. With `ZINN_DIR`, an unusable selected `settings.json` sets only `defaultProject.error` and `telemetry.error`, since the store is already chosen. SQLite may perform normal file housekeeping and crash recovery while `info` inspects a database.
