detached e425dbb

feat(systemprompt): $ZOT_HOME/SYSTEM.md as a persistent override

Resolution order for the system prompt is now:

  1. --system-prompt <text>    (per-run override; highest)
  2. $ZOT_HOME/SYSTEM.md       (persistent user override; new)
  3. built-in defaultIdentity + defaultGuidelines

When SYSTEM.md exists and --system-prompt is empty, its contents
replace the entire identity + guidelines block (same semantics
as --system-prompt). The tool list + skill manifest + appended
sections + date/cwd footer are still added on top, so the file
should contain just the identity / behavior text.

readUserSystemPrompt swallows file errors on purpose: a missing
or unreadable SYSTEM.md falls back to the built-in default
rather than crashing the run. Cached on Resolved.systemCustom
so MergeExtensionTools' system-prompt rebuild path also picks
up the override.

End-to-end verified live:
  with SYSTEM.md (pirate persona): "Arrr, I be Zot, a scallywag..."
  without:                         "I'm zot, a lightweight terminal..."

Documented in README's $ZOT_HOME tree + the --system-prompt
flag note.
commit
e425dbba5971fe3f51c0e051406e5370a6458365
author
patriceckhart · · 2026-04-19 13:47 UTC
parent
84dbd86
signature
unsigned
refs
tag: v0.0.29
2 files · +34 -3
M README.md +4 -1
@@ -102,9 +102,12 @@ $ZOT_HOME/
102102 ├── auth.json # api keys and oauth tokens (mode 0600)
103103 ├── sessions/ # jsonl transcripts, one dir per cwd
104104 ├── models-cache.json # live /v1/models discovery cache (6h ttl)
105+├── SYSTEM.md # optional: replaces the default system prompt
105106 └── logs/ # app log files
106107 ```
107108
109+drop a `SYSTEM.md` in `$ZOT_HOME` to replace the built-in identity + guidelines for every run. `--system-prompt` still wins per-invocation. delete the file to revert to the default.
110+
108111 ## usage
109112
110113 ```bash
@@ -126,7 +129,7 @@ zot --help
126129 | `--model <id>` | pick the model (see `--list-models`) |
127130 | `--api-key <key>` | override api key |
128131 | `--base-url <url>` | override provider base url (tests / self-hosted) |
129-| `--system-prompt <text>` | replace the default system prompt |
132+| `--system-prompt <text>` | replace the default system prompt for this run (also overrides `$ZOT_HOME/SYSTEM.md`) |
130133 | `--append-system-prompt <text>` | append text to the system prompt (repeatable) |
131134 | `--reasoning low\|medium\|high` | enable reasoning on supported models |
132135 | `-c`, `--continue` | resume the latest session for this cwd |
@@ -102,9 +102,12 @@ $ZOT_HOME/
102 ├── auth.json # api keys and oauth tokens (mode 0600)102 ├── auth.json # api keys and oauth tokens (mode 0600)
103 ├── sessions/ # jsonl transcripts, one dir per cwd103 ├── sessions/ # jsonl transcripts, one dir per cwd
104 ├── models-cache.json # live /v1/models discovery cache (6h ttl)104 ├── models-cache.json # live /v1/models discovery cache (6h ttl)
105+├── SYSTEM.md # optional: replaces the default system prompt
105 └── logs/ # app log files106 └── logs/ # app log files
106 ```107 ```
107 108
109+drop a `SYSTEM.md` in `$ZOT_HOME` to replace the built-in identity + guidelines for every run. `--system-prompt` still wins per-invocation. delete the file to revert to the default.
110+
108 ## usage111 ## usage
109 112
110 ```bash113 ```bash
@@ -126,7 +129,7 @@ zot --help
126 | `--model <id>` | pick the model (see `--list-models`) |129 | `--model <id>` | pick the model (see `--list-models`) |
127 | `--api-key <key>` | override api key |130 | `--api-key <key>` | override api key |
128 | `--base-url <url>` | override provider base url (tests / self-hosted) |131 | `--base-url <url>` | override provider base url (tests / self-hosted) |
129-| `--system-prompt <text>` | replace the default system prompt |132+| `--system-prompt <text>` | replace the default system prompt for this run (also overrides `$ZOT_HOME/SYSTEM.md`) |
130 | `--append-system-prompt <text>` | append text to the system prompt (repeatable) |133 | `--append-system-prompt <text>` | append text to the system prompt (repeatable) |
131 | `--reasoning low\|medium\|high` | enable reasoning on supported models |134 | `--reasoning low\|medium\|high` | enable reasoning on supported models |
132 | `-c`, `--continue` | resume the latest session for this cwd |135 | `-c`, `--continue` | resume the latest session for this cwd |
M internal/agent/build.go +30 -2
@@ -3,6 +3,8 @@ package agent
33 import (
44 "fmt"
55 "os"
6+ "path/filepath"
7+ "strings"
68
79 "github.com/patriceckhart/zot/internal/agent/tools"
810 "github.com/patriceckhart/zot/internal/core"
@@ -199,10 +201,19 @@ func Resolve(args Args, requireCred bool) (Resolved, error) {
199201 append_ = append(append_, skillAddendum)
200202 }
201203
204+ // Custom system prompt resolution order:
205+ // 1. --system-prompt flag (highest priority; ad-hoc per run)
206+ // 2. $ZOT_HOME/SYSTEM.md (persistent user override)
207+ // 3. built-in default (defaultIdentity + defaultGuidelines)
208+ custom := args.SystemPrompt
209+ if custom == "" {
210+ custom = readUserSystemPrompt(ZotHome())
211+ }
212+
202213 sys := BuildSystemPrompt(SystemPromptOpts{
203214 CWD: args.CWD,
204215 Tools: summaries,
205- Custom: args.SystemPrompt,
216+ Custom: custom,
206217 Append: append_,
207218 })
208219
@@ -229,11 +240,28 @@ func Resolve(args Args, requireCred bool) (Resolved, error) {
229240 Sandbox: sandbox,
230241 SkillTool: skillTool,
231242 systemAppend: append_,
232- systemCustom: args.SystemPrompt,
243+ systemCustom: custom,
233244 toolDescriptions: descMapFromSummaries(summaries),
234245 }, nil
235246 }
236247
248+// readUserSystemPrompt looks for $ZOT_HOME/SYSTEM.md and returns its
249+// trimmed contents, or "" when the file is missing / unreadable /
250+// empty. Errors are intentionally swallowed: the file is optional,
251+// and any failure to read it should fall back to the built-in
252+// default system prompt rather than crash the run.
253+func readUserSystemPrompt(zotHome string) string {
254+ if zotHome == "" {
255+ return ""
256+ }
257+ path := filepath.Join(zotHome, "SYSTEM.md")
258+ raw, err := os.ReadFile(path)
259+ if err != nil {
260+ return ""
261+ }
262+ return strings.TrimSpace(string(raw))
263+}
264+
237265 // descMapFromSummaries indexes the human-readable descriptions for
238266 // the renderToolsSection rebuild path.
239267 func descMapFromSummaries(summaries []ToolSummary) map[string]string {
@@ -3,6 +3,8 @@ package agent
3 import (3 import (
4 "fmt"4 "fmt"
5 "os"5 "os"
6+ "path/filepath"
7+ "strings"
6 8
7 "github.com/patriceckhart/zot/internal/agent/tools"9 "github.com/patriceckhart/zot/internal/agent/tools"
8 "github.com/patriceckhart/zot/internal/core"10 "github.com/patriceckhart/zot/internal/core"
@@ -199,10 +201,19 @@ func Resolve(args Args, requireCred bool) (Resolved, error) {
199 append_ = append(append_, skillAddendum)201 append_ = append(append_, skillAddendum)
200 }202 }
201 203
204+ // Custom system prompt resolution order:
205+ // 1. --system-prompt flag (highest priority; ad-hoc per run)
206+ // 2. $ZOT_HOME/SYSTEM.md (persistent user override)
207+ // 3. built-in default (defaultIdentity + defaultGuidelines)
208+ custom := args.SystemPrompt
209+ if custom == "" {
210+ custom = readUserSystemPrompt(ZotHome())
211+ }
212+
202 sys := BuildSystemPrompt(SystemPromptOpts{213 sys := BuildSystemPrompt(SystemPromptOpts{
203 CWD: args.CWD,214 CWD: args.CWD,
204 Tools: summaries,215 Tools: summaries,
205- Custom: args.SystemPrompt,216+ Custom: custom,
206 Append: append_,217 Append: append_,
207 })218 })
208 219
@@ -229,11 +240,28 @@ func Resolve(args Args, requireCred bool) (Resolved, error) {
229 Sandbox: sandbox,240 Sandbox: sandbox,
230 SkillTool: skillTool,241 SkillTool: skillTool,
231 systemAppend: append_,242 systemAppend: append_,
232- systemCustom: args.SystemPrompt,243+ systemCustom: custom,
233 toolDescriptions: descMapFromSummaries(summaries),244 toolDescriptions: descMapFromSummaries(summaries),
234 }, nil245 }, nil
235 }246 }
236 247
248+// readUserSystemPrompt looks for $ZOT_HOME/SYSTEM.md and returns its
249+// trimmed contents, or "" when the file is missing / unreadable /
250+// empty. Errors are intentionally swallowed: the file is optional,
251+// and any failure to read it should fall back to the built-in
252+// default system prompt rather than crash the run.
253+func readUserSystemPrompt(zotHome string) string {
254+ if zotHome == "" {
255+ return ""
256+ }
257+ path := filepath.Join(zotHome, "SYSTEM.md")
258+ raw, err := os.ReadFile(path)
259+ if err != nil {
260+ return ""
261+ }
262+ return strings.TrimSpace(string(raw))
263+}
264+
237 // descMapFromSummaries indexes the human-readable descriptions for265 // descMapFromSummaries indexes the human-readable descriptions for
238 // the renderToolsSection rebuild path.266 // the renderToolsSection rebuild path.
239 func descMapFromSummaries(summaries []ToolSummary) map[string]string {267 func descMapFromSummaries(summaries []ToolSummary) map[string]string {

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