Align XDG state home behavior
- commit
- d1e93a76ab10efcdb61f98adce7aba7afab64ba3
- author
- patriceckhart · · 2026-07-12 07:52 UTC
- parent
- 0ae115b
- signature
- unsigned
7 files · +40 -13
M README.md +5 -4
| @@ -73,9 +73,10 @@ The easiest way is to just run `zot` and type `/login`. The TUI opens even witho | |||
| 73 | 73 | 3. `$ZOT_HOME/auth.json` (API key or OAuth token; mode 0600) | |
| 74 | 74 | ||
| 75 | 75 | `$ZOT_HOME` defaults to: | |
| 76 | - | - macOS: `~/Library/Application Support/zot` | |
| 77 | - | - Linux: `$XDG_STATE_HOME/zot` or `~/.local/state/zot` | |
| 78 | - | - Windows: `%LOCALAPPDATA%\zot` | |
| 76 | + | - All platforms: `$XDG_STATE_HOME/zot` when `XDG_STATE_HOME` is set | |
| 77 | + | - macOS fallback: `~/Library/Application Support/zot` | |
| 78 | + | - Linux fallback: `~/.local/state/zot` | |
| 79 | + | - Windows fallback: `%LOCALAPPDATA%\zot` | |
| 79 | 80 | ||
| 80 | 81 | ### `/login` flow | |
| 81 | 82 | ||
| @@ -392,7 +393,7 @@ No configuration is required — the candidate list is built dynamically from yo | |||
| 392 | 393 | ||
| 393 | 394 | ### Custom models | |
| 394 | 395 | ||
| 395 | - | Place a `models.json` in `$ZOT_HOME` (macOS: `~/Library/Application Support/zot/`, Linux: `~/.local/state/zot/`) to add models that aren't in the baked-in catalog or to override existing entries: | |
| 396 | + | Place a `models.json` in `$ZOT_HOME` (`$XDG_STATE_HOME/zot/` when set, otherwise the platform default above) to add models that aren't in the baked-in catalog or to override existing entries: | |
| 396 | 397 | ||
| 397 | 398 | ```json | |
| 398 | 399 | { | |
| @@ -73,9 +73,10 @@ The easiest way is to just run `zot` and type `/login`. The TUI opens even witho | |||||
| 73 | 3. `$ZOT_HOME/auth.json` (API key or OAuth token; mode 0600) | 73 | 3. `$ZOT_HOME/auth.json` (API key or OAuth token; mode 0600) | ||
| 74 | 74 | ||||
| 75 | `$ZOT_HOME` defaults to: | 75 | `$ZOT_HOME` defaults to: | ||
| 76 | - | - macOS: `~/Library/Application Support/zot` | 76 | + | - All platforms: `$XDG_STATE_HOME/zot` when `XDG_STATE_HOME` is set |
| 77 | - | - Linux: `$XDG_STATE_HOME/zot` or `~/.local/state/zot` | 77 | + | - macOS fallback: `~/Library/Application Support/zot` |
| 78 | - | - Windows: `%LOCALAPPDATA%\zot` | 78 | + | - Linux fallback: `~/.local/state/zot` |
| 79 | + | - Windows fallback: `%LOCALAPPDATA%\zot` | |||
| 79 | 80 | ||||
| 80 | ### `/login` flow | 81 | ### `/login` flow | ||
| 81 | 82 | ||||
| @@ -392,7 +393,7 @@ No configuration is required — the candidate list is built dynamically from yo | |||||
| 392 | 393 | ||||
| 393 | ### Custom models | 394 | ### Custom models | ||
| 394 | 395 | ||||
| 395 | - | Place a `models.json` in `$ZOT_HOME` (macOS: `~/Library/Application Support/zot/`, Linux: `~/.local/state/zot/`) to add models that aren't in the baked-in catalog or to override existing entries: | 396 | + | Place a `models.json` in `$ZOT_HOME` (`$XDG_STATE_HOME/zot/` when set, otherwise the platform default above) to add models that aren't in the baked-in catalog or to override existing entries: |
| 396 | 397 | ||||
| 397 | ```json | 398 | ```json | ||
| 398 | { | 399 | { | ||
M docs/extensions.md +3 -2
| @@ -97,8 +97,9 @@ zot scans two directories on startup, in this order: | |||
| 97 | 97 | 2. **Global**: `$ZOT_HOME/extensions/<name>/extension.json` | |
| 98 | 98 | ||
| 99 | 99 | A project-local extension with the same name wins over a global one. | |
| 100 | - | On macOS `$ZOT_HOME` defaults to `~/Library/Application Support/zot/`; | |
| 101 | - | on Linux it's `$XDG_STATE_HOME/zot` or `~/.local/state/zot`. | |
| 100 | + | When `XDG_STATE_HOME` is set on any platform, `$ZOT_HOME` defaults to | |
| 101 | + | `$XDG_STATE_HOME/zot`. Otherwise it defaults to `~/Library/Application Support/zot/` | |
| 102 | + | on macOS, `~/.local/state/zot` on Linux, or `%LOCALAPPDATA%\zot` on Windows. | |
| 102 | 103 | ||
| 103 | 104 | Because each extension owns its own directory, the recommended place | |
| 104 | 105 | for extension state is inside that directory itself (for example | |
| @@ -97,8 +97,9 @@ zot scans two directories on startup, in this order: | |||||
| 97 | 2. **Global**: `$ZOT_HOME/extensions/<name>/extension.json` | 97 | 2. **Global**: `$ZOT_HOME/extensions/<name>/extension.json` | ||
| 98 | 98 | ||||
| 99 | A project-local extension with the same name wins over a global one. | 99 | A project-local extension with the same name wins over a global one. | ||
| 100 | - | On macOS `$ZOT_HOME` defaults to `~/Library/Application Support/zot/`; | 100 | + | When `XDG_STATE_HOME` is set on any platform, `$ZOT_HOME` defaults to |
| 101 | - | on Linux it's `$XDG_STATE_HOME/zot` or `~/.local/state/zot`. | 101 | + | `$XDG_STATE_HOME/zot`. Otherwise it defaults to `~/Library/Application Support/zot/` |
| 102 | + | on macOS, `~/.local/state/zot` on Linux, or `%LOCALAPPDATA%\zot` on Windows. | |||
| 102 | 103 | ||||
| 103 | Because each extension owns its own directory, the recommended place | 104 | Because each extension owns its own directory, the recommended place | ||
| 104 | for extension state is inside that directory itself (for example | 105 | for extension state is inside that directory itself (for example | ||
M docs/skills.md +3 -2
| @@ -64,8 +64,9 @@ skill ecosystem works in zot unchanged. Drop your existing | |||
| 64 | 64 | `.claude/skills/` or `.agents/skills/` directories into a project and | |
| 65 | 65 | zot will pick them up. | |
| 66 | 66 | ||
| 67 | - | `$ZOT_HOME` defaults to `~/Library/Application Support/zot/` on macOS, | |
| 68 | - | `$XDG_STATE_HOME/zot` on Linux, `%LOCALAPPDATA%\zot` on Windows. | |
| 67 | + | When `XDG_STATE_HOME` is set on any platform, `$ZOT_HOME` defaults to | |
| 68 | + | `$XDG_STATE_HOME/zot`. Otherwise it defaults to `~/Library/Application Support/zot/` | |
| 69 | + | on macOS, `~/.local/state/zot` on Linux, or `%LOCALAPPDATA%\zot` on Windows. | |
| 69 | 70 | ||
| 70 | 71 | ## Inspecting installed skills | |
| 71 | 72 | ||
| @@ -64,8 +64,9 @@ skill ecosystem works in zot unchanged. Drop your existing | |||||
| 64 | `.claude/skills/` or `.agents/skills/` directories into a project and | 64 | `.claude/skills/` or `.agents/skills/` directories into a project and | ||
| 65 | zot will pick them up. | 65 | zot will pick them up. | ||
| 66 | 66 | ||||
| 67 | - | `$ZOT_HOME` defaults to `~/Library/Application Support/zot/` on macOS, | 67 | + | When `XDG_STATE_HOME` is set on any platform, `$ZOT_HOME` defaults to |
| 68 | - | `$XDG_STATE_HOME/zot` on Linux, `%LOCALAPPDATA%\zot` on Windows. | 68 | + | `$XDG_STATE_HOME/zot`. Otherwise it defaults to `~/Library/Application Support/zot/` |
| 69 | + | on macOS, `~/.local/state/zot` on Linux, or `%LOCALAPPDATA%\zot` on Windows. | |||
| 69 | 70 | ||||
| 70 | ## Inspecting installed skills | 71 | ## Inspecting installed skills | ||
| 71 | 72 | ||||
M examples/extensions/mcp-bridge/Makefile +3 -3
| @@ -4,10 +4,10 @@ BIN := $(notdir $(CURDIR)) | |||
| 4 | 4 | # Must match "name" in extension.json (slash command/logical extension name). | |
| 5 | 5 | EXT := mcp | |
| 6 | 6 | ZOT_HOME ?= $(shell \ | |
| 7 | - | if [ "$$(uname -s)" = "Darwin" ]; then \ | |
| 8 | - | printf '%s/Library/Application Support/zot' "$$HOME"; \ | |
| 9 | - | elif [ -n "$$XDG_STATE_HOME" ]; then \ | |
| 7 | + | if [ -n "$$XDG_STATE_HOME" ]; then \ | |
| 10 | 8 | printf '%s/zot' "$$XDG_STATE_HOME"; \ | |
| 9 | + | elif [ "$$(uname -s)" = "Darwin" ]; then \ | |
| 10 | + | printf '%s/Library/Application Support/zot' "$$HOME"; \ | |
| 11 | 11 | else \ | |
| 12 | 12 | printf '%s/.local/state/zot' "$$HOME"; \ | |
| 13 | 13 | fi) | |
| @@ -4,10 +4,10 @@ BIN := $(notdir $(CURDIR)) | |||||
| 4 | # Must match "name" in extension.json (slash command/logical extension name). | 4 | # Must match "name" in extension.json (slash command/logical extension name). | ||
| 5 | EXT := mcp | 5 | EXT := mcp | ||
| 6 | ZOT_HOME ?= $(shell \ | 6 | ZOT_HOME ?= $(shell \ | ||
| 7 | - | if [ "$$(uname -s)" = "Darwin" ]; then \ | 7 | + | if [ -n "$$XDG_STATE_HOME" ]; then \ |
| 8 | - | printf '%s/Library/Application Support/zot' "$$HOME"; \ | |||
| 9 | - | elif [ -n "$$XDG_STATE_HOME" ]; then \ | |||
| 10 | printf '%s/zot' "$$XDG_STATE_HOME"; \ | 8 | printf '%s/zot' "$$XDG_STATE_HOME"; \ | ||
| 9 | + | elif [ "$$(uname -s)" = "Darwin" ]; then \ | |||
| 10 | + | printf '%s/Library/Application Support/zot' "$$HOME"; \ | |||
| 11 | else \ | 11 | else \ | ||
| 12 | printf '%s/.local/state/zot' "$$HOME"; \ | 12 | printf '%s/.local/state/zot' "$$HOME"; \ | ||
| 13 | fi) | 13 | fi) | ||
M examples/extensions/mcp-bridge/README.md +1 -1
| @@ -61,7 +61,7 @@ Config files are loaded from two locations (project overrides global per-server) | |||
| 61 | 61 | ||
| 62 | 62 | | Location | Scope | | |
| 63 | 63 | |---|---| | |
| 64 | - | | `$ZOT_HOME/mcp.json` | Global (macOS: `~/Library/Application Support/zot/mcp.json`) | | |
| 64 | + | | `$ZOT_HOME/mcp.json` | Global (`$XDG_STATE_HOME/zot/mcp.json` when `XDG_STATE_HOME` is set) | | |
| 65 | 65 | | `.zot/mcp.json` | Project-level (in your project root) | | |
| 66 | 66 | ||
| 67 | 67 | ### Config Format | |
| @@ -61,7 +61,7 @@ Config files are loaded from two locations (project overrides global per-server) | |||||
| 61 | 61 | ||||
| 62 | | Location | Scope | | 62 | | Location | Scope | | ||
| 63 | |---|---| | 63 | |---|---| | ||
| 64 | - | | `$ZOT_HOME/mcp.json` | Global (macOS: `~/Library/Application Support/zot/mcp.json`) | | 64 | + | | `$ZOT_HOME/mcp.json` | Global (`$XDG_STATE_HOME/zot/mcp.json` when `XDG_STATE_HOME` is set) | |
| 65 | | `.zot/mcp.json` | Project-level (in your project root) | | 65 | | `.zot/mcp.json` | Project-level (in your project root) | | ||
| 66 | 66 | ||||
| 67 | ### Config Format | 67 | ### Config Format | ||
M examples/extensions/mcp-bridge/config.go +1 -1
| @@ -3,7 +3,7 @@ | |||
| 3 | 3 | // Reads standard MCP config files (same format as Claude Desktop, Cursor, etc.) | |
| 4 | 4 | // from two locations: | |
| 5 | 5 | // | |
| 6 | - | // 1. Global: $ZOT_HOME/mcp.json (macOS: ~/Library/Application Support/zot/mcp.json) | |
| 6 | + | // 1. Global: $ZOT_HOME/mcp.json | |
| 7 | 7 | // 2. Project: .zot/mcp.json (in the current working directory) | |
| 8 | 8 | // | |
| 9 | 9 | // Project config overrides global config per-server (shallow merge). | |
| @@ -3,7 +3,7 @@ | |||||
| 3 | // Reads standard MCP config files (same format as Claude Desktop, Cursor, etc.) | 3 | // Reads standard MCP config files (same format as Claude Desktop, Cursor, etc.) | ||
| 4 | // from two locations: | 4 | // from two locations: | ||
| 5 | // | 5 | // | ||
| 6 | - | // 1. Global: $ZOT_HOME/mcp.json (macOS: ~/Library/Application Support/zot/mcp.json) | 6 | + | // 1. Global: $ZOT_HOME/mcp.json |
| 7 | // 2. Project: .zot/mcp.json (in the current working directory) | 7 | // 2. Project: .zot/mcp.json (in the current working directory) | ||
| 8 | // | 8 | // | ||
| 9 | // Project config overrides global config per-server (shallow merge). | 9 | // Project config overrides global config per-server (shallow merge). | ||
A packages/agent/config_home_test.go +24 -0
| @@ -0,0 +1,24 @@ | |||
| 1 | + | package agent | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "path/filepath" | |
| 5 | + | "testing" | |
| 6 | + | ) | |
| 7 | + | ||
| 8 | + | func TestZotHomePrefersExplicitHomeOverXDGStateHome(t *testing.T) { | |
| 9 | + | t.Setenv("ZOT_HOME", filepath.Join("explicit", "zot")) | |
| 10 | + | t.Setenv("XDG_STATE_HOME", filepath.Join("xdg", "state")) | |
| 11 | + | ||
| 12 | + | if got, want := ZotHome(), filepath.Join("explicit", "zot"); got != want { | |
| 13 | + | t.Fatalf("ZotHome() = %q, want %q", got, want) | |
| 14 | + | } | |
| 15 | + | } | |
| 16 | + | ||
| 17 | + | func TestZotHomeUsesXDGStateHome(t *testing.T) { | |
| 18 | + | t.Setenv("ZOT_HOME", "") | |
| 19 | + | t.Setenv("XDG_STATE_HOME", filepath.Join("xdg", "state")) | |
| 20 | + | ||
| 21 | + | if got, want := ZotHome(), filepath.Join("xdg", "state", "zot"); got != want { | |
| 22 | + | t.Fatalf("ZotHome() = %q, want %q", got, want) | |
| 23 | + | } | |
| 24 | + | } | |
| @@ -0,0 +1,24 @@ | |||||
| 1 | + | package agent | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "path/filepath" | |||
| 5 | + | "testing" | |||
| 6 | + | ) | |||
| 7 | + | ||||
| 8 | + | func TestZotHomePrefersExplicitHomeOverXDGStateHome(t *testing.T) { | |||
| 9 | + | t.Setenv("ZOT_HOME", filepath.Join("explicit", "zot")) | |||
| 10 | + | t.Setenv("XDG_STATE_HOME", filepath.Join("xdg", "state")) | |||
| 11 | + | ||||
| 12 | + | if got, want := ZotHome(), filepath.Join("explicit", "zot"); got != want { | |||
| 13 | + | t.Fatalf("ZotHome() = %q, want %q", got, want) | |||
| 14 | + | } | |||
| 15 | + | } | |||
| 16 | + | ||||
| 17 | + | func TestZotHomeUsesXDGStateHome(t *testing.T) { | |||
| 18 | + | t.Setenv("ZOT_HOME", "") | |||
| 19 | + | t.Setenv("XDG_STATE_HOME", filepath.Join("xdg", "state")) | |||
| 20 | + | ||||
| 21 | + | if got, want := ZotHome(), filepath.Join("xdg", "state", "zot"); got != want { | |||
| 22 | + | t.Fatalf("ZotHome() = %q, want %q", got, want) | |||
| 23 | + | } | |||
| 24 | + | } | |||