detached d1e93a7

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
7373 3. `$ZOT_HOME/auth.json` (API key or OAuth token; mode 0600)
7474
7575 `$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`
7980
8081 ### `/login` flow
8182
@@ -392,7 +393,7 @@ No configuration is required — the candidate list is built dynamically from yo
392393
393394 ### Custom models
394395
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:
396397
397398 ```json
398399 {
@@ -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` flow81 ### `/login` flow
81 82
@@ -392,7 +393,7 @@ No configuration is required — the candidate list is built dynamically from yo
392 393
393 ### Custom models394 ### 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 ```json398 ```json
398 {399 {
M docs/extensions.md +3 -2
@@ -97,8 +97,9 @@ zot scans two directories on startup, in this order:
9797 2. **Global**: `$ZOT_HOME/extensions/<name>/extension.json`
9898
9999 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.
102103
103104 Because each extension owns its own directory, the recommended place
104105 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 place104 Because each extension owns its own directory, the recommended place
104 for extension state is inside that directory itself (for example105 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
6464 `.claude/skills/` or `.agents/skills/` directories into a project and
6565 zot will pick them up.
6666
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.
6970
7071 ## Inspecting installed skills
7172
@@ -64,8 +64,9 @@ skill ecosystem works in zot unchanged. Drop your existing
64 `.claude/skills/` or `.agents/skills/` directories into a project and64 `.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 skills71 ## Inspecting installed skills
71 72
M examples/extensions/mcp-bridge/Makefile +3 -3
@@ -4,10 +4,10 @@ BIN := $(notdir $(CURDIR))
44 # Must match "name" in extension.json (slash command/logical extension name).
55 EXT := mcp
66 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 \
108 printf '%s/zot' "$$XDG_STATE_HOME"; \
9+ elif [ "$$(uname -s)" = "Darwin" ]; then \
10+ printf '%s/Library/Application Support/zot' "$$HOME"; \
1111 else \
1212 printf '%s/.local/state/zot' "$$HOME"; \
1313 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 := mcp5 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)
6161
6262 | Location | Scope |
6363 |---|---|
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) |
6565 | `.zot/mcp.json` | Project-level (in your project root) |
6666
6767 ### 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 Format67 ### Config Format
M examples/extensions/mcp-bridge/config.go +1 -1
@@ -3,7 +3,7 @@
33 // Reads standard MCP config files (same format as Claude Desktop, Cursor, etc.)
44 // from two locations:
55 //
6-// 1. Global: $ZOT_HOME/mcp.json (macOS: ~/Library/Application Support/zot/mcp.json)
6+// 1. Global: $ZOT_HOME/mcp.json
77 // 2. Project: .zot/mcp.json (in the current working directory)
88 //
99 // 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+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close