Nyia Keeper Configuration Reference¶
This document is the single reference for Nyia Keeper's user configuration: how settings are stored,
the scope precedence, how to set them (CLI or by hand), and the convention that decides what is a
nyia config key versus a structural file.
The nyia config system¶
User settings are registered keys managed by nyia config:
nyia config view [assistant] # effective values + their source
nyia config get <key>
nyia config project [assistant] key=value # write project scope
nyia config global [assistant] key=value # write global scope
Registered keys (as of this audit)¶
| Short name | Internal name | Validation | Notes |
|---|---|---|---|
command_mode |
NYIA_COMMAND_MODE |
safe|full |
dangerous-value confirm gate for full global |
rag_model |
NYIA_RAG_MODEL |
model string | also settable in <assistant>.conf (see "already-both") |
team_dir |
NYIA_TEAM_DIR |
path | local folder of shared skills/personas/prompts (you sync it) |
workspace_sync |
NYIA_WORKSPACE_SYNC |
bool | also a workspace.conf directive (sync_branches, higher precedence) |
whatsup_enabled |
NYIA_WHATSUP_ENABLED |
bool | |
whatsup_auto_read |
NYIA_WHATSUP_AUTO_READ |
kickoff | never |
|
auto_branch |
NYIA_AUTO_BRANCH |
bool | auto-create a timestamped work branch |
hide_footprint |
NYIA_HIDE_FOOTPRINT |
off | local | global |
where Nyia's ignore rules live; --hide sets local for one launch — see Hiding Nyia's footprint |
output_emoji |
NYIA_OUTPUT_EMOJI |
off | on |
Unicode emojis in chat replies, default off; an invalid value gives a warning and off. ASCII emoticons such as ;) are always allowed. See Output style |
auth_profile |
NYIA_AUTH_PROFILE |
profile-name slug | global scope only; selects the active profile — a separate account; content is inherited from your default unless the profile is a persona — see Profiles. Also settable per-command with --profile <name> |
Host-only, hand-edited: NYIA_EXCLUSIONS_DEEP_SCAN=true in the global ~/.config/nyiakeeper/config/nyia.conf makes every launch also search inside dependency directories for exclusions (see Mount Exclusions, "Deep scan"). It is not a nyia config key and no project tier is read for it: a session can write the project files.
Scope precedence (highest → lowest)¶
- CLI flag / env
- Project private —
.nyiakeeper/private/config/nyia.conf(hand-edit only) - Project global —
.nyiakeeper/nyia.conf(written bynyia config project) - Project shared —
.nyiakeeper/shared/config/nyia.conf(hand-edit only; committed downstream) - Global + assistant —
~/.config/nyiakeeper/config/<assistant>.conf - Global —
~/.config/nyiakeeper/config/nyia.conf(written bynyia config global)
(The former team tier — $NYIA_TEAM_DIR/config/ — was removed in Plan 328a: a team directory carries skills,
personas and prompt overlays, never config values.)
Hiding Nyia's footprint (hide_footprint)¶
Nyia keeps its files out of git with ignore rules. hide_footprint decides where those rules live:
| Value | .nyiakeeper/ + assistant dirs (.claude/, .codex/, .opencode/, .vibe/) |
|---|---|
off (default) |
one marked block in the project's tracked .gitignore |
local |
.git/info/exclude (this clone only); Nyia's rules are removed from .gitignore |
global |
not implemented yet: behaves as local and says so at each launch |
Nyia no longer puts a prompt file (CLAUDE.md, AGENTS.md, ...) at the project root, so there is no
prompt rule any more (see How each CLI gets the Nyia prompt).
nyia config project hide_footprint=local # this project
nyia config global hide_footprint=local # every project
nyia-claude --hide # this launch only (wins over config)
Precedence is the usual one (CLI flag, then project, then global; see above). Setting the key prints a
read-only preview of the current project: where each rule group lives, what is still tracked, and whether
Nyia's .gitignore rules are still there. Nothing moves until the next launch. With local, a launch also
reports problems (add --verbose for the full picture).
How the .gitignore block behaves (off)¶
- Nyia writes the block once, between
# >>> nyia-keeper footprint ... >>>and# <<< nyia-keeper footprint <<<. Once the begin marker exists, Nyia never writes.gitignoreagain, so a line you delete (or a block you empty) stays deleted. - A project that already has Nyia's lines from an older version keeps them where they are: the block only holds the entries that were missing (or just a comment, if none were). Nothing is reordered or removed.
- No prompt-file rule is written any more. A
/CLAUDE.md(orAGENTS.md, ...) line an older version put in Nyia'sinfo/excludeblock is removed once the old root link it covered is gone (the session removes that link). A bare, unmarkedCLAUDE.mdline from before the block existed hides that name at any depth; Nyia leaves it as it is and warns at each launch: delete it by hand. - The
info/excludeblock is managed (rewritten when an assistant is added or the mode changes). To override a line in it, add your own rule after the block: later rules win within one file.
What local does, and its honest limits¶
.gitignoreoutranksinfo/exclude, so hiding only works once Nyia's rules are out of.gitignore. The firstlocallaunch writes theinfo/excludeblock, checks it, then removes Nyia's marked block and the bare.nyiakeeperlines from.gitignore, and tells you to commit.gitignore. If git does not then nameinfo/excludeas the file that ignores.nyiakeeper/private/, the old.gitignoreis put back. A malformed block is never edited (fix it by hand). Bare assistant-dir lines (.claude/, ...) are left and reported: you may have written them yourself.- It cannot hide a tracked file. Files already committed stay visible; Nyia names them and never runs
git rm --cachedfor you. - It is per clone, per person. Each clone needs it, and a teammate who launches Nyia without
hide_footprintadds the.gitignoreblock back. - Only a real
.gitdirectory is written. In a linked worktree or a submodule (where.gitis a file), or when.gitis a link or redirected, Nyia writes noinfo/excludeand says so inlocalmode: a session can plant those to point at another repository on your machine. Inlocalmode the.gitignorerules are then left in place, so nothing is hidden there. - It turns off team sharing.
.nyiakeeper/shared/,exclusions.conf,workspace.confandwhatsup/entries are ignored too, so teammates do not receive your changes to them. Nyia warns when such content exists. - A committed root
CLAUDE.md/AGENTS.mdlink into.nyiakeeper/(from an older version) is a dangling link for everyone who clones: Nyia reports it and never removes a tracked file;git rmit. Your own trackedCLAUDE.md/AGENTS.mdis not Nyia's and is not reported. To share prompt content, use.nyiakeeper/shared/prompts/(see Team sharing). - Workspace mode: the rules apply to the workspace root's repository only; member repositories are never written.
git_history_cutoff: the assistant's in-container.gitdoes not carryinfo/exclude, so git inside the session may show Nyia's files as untracked when both are on.
Editing config by hand¶
nyia config writes plain shell-style lines using the internal key name:
# .nyiakeeper/nyia.conf (project) or ~/.config/nyiakeeper/config/nyia.conf (global)
NYIA_COMMAND_MODE="full"
Both the dev (
bin/nyia) and runtime (dist/runtime/bin/nyia) editions share one implementation of these config commands (lib/config-commands.sh, Plan 282), soview/project/globalbehave identically in both.nyia config getreturns only infrastructure paths (home,config,data) — usenyia config viewfor key values.
These files are fully hand-editable and honored on read. Caveats:
- Use the internal NYIA_* name, not the short name. A line like command_mode=full is silently
ignored (the safe parser only accepts allow-listed NYIA_* keys).
- The CLI writes only project and global scopes. private / shared / team are hand-edit-only.
- Hand-edits skip validation. nyia config set validates; editing the file by hand does not — an
invalid value may be silently corrected (e.g. command_mode falls back to safe) or, for keys without
resolver-side validation, used as-is. You own value-correctness when hand-editing.
Secrets — never in trackable scopes¶
API keys are mostly stored as env-var references, not values: API_KEY_ENV="OPENAI_API_KEY" names the
variable, it does not hold the secret. The exception is MISTRAL_API_KEY, which vibe.conf invites as a
literal value. Rule: secret values must never be written to the project / shared / team scopes
(those are designed to be committed in downstream user projects). Keep secrets in the environment, the
global scope, or a private creds file.
Convention: config key vs structural file vs subcommand¶
- Single scalars / small enumerated values →
nyia configkeys (discoverable vianyia config view, scoped, validated). - Large or append-managed collections that have their own CLI → structural files (e.g.
exclusions.confvianyia exclusions). Heuristic, not absolute:NYIA_PROTECTED_BRANCHESis a comma-list living as a config scalar (union-merged across scopes) — a list is not automatically structural. - Subcommands do operations, not settings. A config key holds a setting; a subcommand runs an
operation. New domain features follow this shape (e.g.
git_history_cutoffkey +nyia git-historyops;network_egress_policykey + the egress firewall).
"Already-both" settings (two sources, by design)¶
Some settings exist as a config key AND elsewhere, with a defined precedence:
- workspace_sync (key) ← overridden by the workspace.conf sync_branches directive.
- rag_model (key) ← a project <assistant>.conf NYIA_RAG_MODEL (precedence level 5) outranks
nyia config global rag_model (level 6). This is the canonical two-source example.
How each CLI gets the Nyia prompt¶
The launcher writes the Nyia prompt to .nyiakeeper/<assistant>/ (CLAUDE.md, AGENTS.md, OPENCODE.md,
VIBE.md) and never touches the project root: your own CLAUDE.md / AGENTS.md there stays yours and
the assistant still loads it, next to the Nyia prompt. The image hands the file to the assistant for that
session only:
| Assistant | How | Combined with yours |
|---|---|---|
| Claude | --append-system-prompt-file=<file> (appended to Claude's system prompt; read whole) |
the project's CLAUDE.md loads natively |
| Codex | -c developer_instructions=<text> (developer role) |
Nyia's -c replaces every developer_instructions codex would read, so it carries them, separated by a blank line, in codex's layer order (see below); a -c developer_instructions=... you pass yourself comes later and replaces all of it; the project's AGENTS.md loads natively |
| OpenCode | the file is appended to instructions in OPENCODE_CONFIG_CONTENT, as an absolute path (OpenCode globs only its file name, so a project path with [, * or spaces works) |
your instructions (config files or an exported value) are kept |
| Vibe | copied to AGENTS.md in the session's VIBE_HOME, read as user-level instructions; vibe's own prompt stays |
a trusted folder's and each --add-dir root's AGENTS.md still load, and outrank it (see below) |
Vibe (delivery measured on vibe 3.0.4 with the real image, TUI and headless; 2.25.8 headless only, outside the image):
- Nyia's text is the lowest-ranked instruction layer. Vibe ranks project instructions above user
instructions, and follows the text with "this context may or may not be relevant". The one header line
Nyia adds is a label; it does not change this.
- Example: a repo or workspace member with an AGENTS.md saying "this repository is writable" can
override Nyia's "read-only" line. The model may then try the write; the read-only mount refuses it. The
mount is the boundary, not the prompt.
- ~/.vibe/AGENTS.md is not read in a Nyia session. Every session can write to it, so it would carry
text from one project to the next.
- include_project_context = false in ~/.vibe/config.toml makes vibe drop every AGENTS.md, Nyia's
included (measured). VIBE_INCLUDE_PROJECT_CONTEXT outranks the file (measured outside the image). The
project's .vibe/config.toml should do the same when vibe trusts the folder: that comes from reading
vibe's config layers, not from a run. Nyia warns for all three and keeps your setting.
- Vibe's full-screen TUI hides what is printed before it. So when the prompt is not delivered, the warning
comes with a 3-second pause (only when stdin and stderr are terminals) and again when the session exits.
The launch is not refused.
- The launcher warns about a missing file or a link only. The image checks size and encoding: a file over
256 KiB, empty, or not UTF-8 starts vibe without the Nyia prompt, with one warning.
Codex developer_instructions, in order (codex 0.166.0 layers: user 20, --profile file 21,
project 25, -c 30; codex-rs/config/src/config_layer_source.rs, codex-rs/config/src/loader/mod.rs):
1. Yours: the top-level value in ~/.codex/config.toml, or, with --profile <name> (-p), the top-level
value of ~/.codex/<name>.config.toml when that file sets one (nyia-codex takes --profile for
its own auth profiles: pass codex's as -p <name>). A legacy [profiles.<name>] table is
ignored: codex has no such key there (codex-rs/config/src/profile_toml.rs). A profile name that is not
codex's plain name (letters, digits, _, -), or a profile file that is a link or unreadable, is not
read (a note says so).
2. The project's: .codex/config.toml at the project root, only when your config trusts the project
([projects."<path>"] trust_level = "trusted"), as codex disables an untrusted project's layer. Otherwise
it is left out with a note, and it does not apply that session. A .codex dir or file that is a link is
never read.
3. Nyia's prompt.
The file must be a regular file (not a link), valid UTF-8 and at most 256 KiB (for Codex, at most 120 KiB
once encoded). Otherwise the session starts without the Nyia prompt and says so; the launcher checks the
same rules right after writing the file and warns before the container starts. A --login container,
served reviews and the plan sentinel get no Nyia prompt (reviews carry the document text only).
What crosses into the container¶
The launcher never forwards your shell environment wholesale. Exactly three channels carry values into the box
(all resolved on the host by create_docker_env_file / run_docker_container):
| Channel | Source | What passes | Rule |
|---|---|---|---|
| Assistant settings allowlist | the global ~/.config/nyiakeeper/config/<assistant>.conf (profile-aware) |
a short, per-assistant list of documented non-secret keys — today only OpenCode: OLLAMA_AUTO_SETUP, ENABLE_OLLAMA, OLLAMA_FILTER_TOOLS (true|false), OLLAMA_HOST (host[:port], no scheme), OLLAMA_DEFAULT_MODEL (model name) |
conf only (a host-shell export of the same name never crosses); an invalid value is dropped with a warning, the launch continues with the in-box default; never read from a project tier — OLLAMA_HOST redirects model and RAG traffic, so a committed file must not set it (same rule as auth_profile) |
| Credentials | .nyiakeeper/private/creds/env (literal-parsed, never sourced) + the four fixed names read from the global conf or the host shell environment (ANTHROPIC_API_KEY, OPENAI_API_KEY, MISTRAL_API_KEY, GOOGLE_CLOUD_PROJECT — pre-existing behaviour) |
names matching *_API_KEY, *_TOKEN, *_KEY, GOOGLE_CLOUD_PROJECT, GOOGLE_APPLICATION_CREDENTIALS |
default-deny allowlist; NYIA_* / OLLAMA_* in creds/env are rejected with a warning |
| Launcher exports | flags and resolved config | NYIA_COMMAND_MODE, NYIA_RAG_MODEL, ENABLE_RAG, NYIA_RAG_VERBOSE (--rag-verbose), NYIA_DEBUG=true (only when the host has it on), NYIA_OPERATION_TYPE=auth on --login, the workspace / egress / git-history markers |
not user-settable except through their own flag or key |
Notes:
- ENABLE_RAG="true" in a global <assistant>.conf is honoured (the conf is sourced after the CLI defaults and
parse_args never resets it) — a supported way to make RAG the default for one assistant.
- A opencode.conf generated before Plan 340 carried the example defaults OLLAMA_HOST="localhost:11434" and
OLLAMA_DEFAULT_MODEL="llama3.2" as active lines; the launcher comments those two exact lines out once (with a note) so
they do not turn into per-launch probes/warnings — uncomment them to enforce the values.
- Anything else you put in <assistant>.conf stays on the host. If a documented key seems to do nothing in the
box, check this table first.
OpenCode configuration — the layers¶
OpenCode merges its configuration natively; Nyia only adds files in slots OpenCode already reads. In the box, lowest to highest precedence:
| # | File (in the box) | Owner | Notes |
|---|---|---|---|
| 1 | ~/.config/opencode/config.json |
Nyia — regenerated every launch (Ollama auto-setup) | never edit; OLLAMA_AUTO_SETUP="false" keeps it as it is |
| 2 | ~/.config/opencode/opencode.json (or .jsonc) |
you — host path ~/.config/nyiakeeper/opencode/opencode.json (profiles/<p>/opencode/ when a profile is active) |
your providers, models, model pin; Nyia never writes or prints it (OpenCode itself adds a "$schema" line on first load — that is the CLI, not Nyia) |
| 3 | $project/opencode.json (or .jsonc) |
the repository | Nyia only reads it (see the threat note below) |
| 4 | OPENCODE_CONFIG_CONTENT (environment, this session only) |
Nyia — the codebase-search MCP server when --rag is on (Plan 393), and the Nyia prompt file appended to instructions (Plan 392); merged over any value you exported (your instructions entries come first) |
no file: nothing is written to $project/.opencode/ any more (an old Nyia entry there is removed at launch) |
| 5 | ~/.opencode/opencode.json — the same host file as layer 2, seen again through Nyia's second mount |
you | so on a conflicting key your file wins over the project file; a repository can only add what you did not set (observed behaviour, not a contract Nyia's code relies on) |
Practical notes:
- Add a cloud / OpenAI-compatible provider in layer 2 ("provider": {"acme": {"npm": "@ai-sdk/openai-compatible", "options": {"baseURL": "…", "apiKey": "{env:ACME_API_KEY}"}, "models": {…}}}) and pin "model": "acme/…" there — otherwise the default flips to whatever Ollama model is detected first, and to nothing when Ollama is down.
- Key delivery: only two paths persist across launches — a name matching *_API_KEY / *_TOKEN / *_KEY in .nyiakeeper/private/creds/env (then {env:NAME}), or a file dropped in the persisted dir ({file:acme.key}, relative to the config file). /connect writes auth.json into OpenCode's XDG data dir, which Nyia does persist on the host since Plan 353 (under ~/.config/nyiakeeper/opencode/, or profiles/<profile>/opencode/ with a profile active; OpenCode writes auth.json itself at 0600, and the containing directories are 0700) — so you no longer re-authenticate every run. Note this means OAuth material and the database's credential tables rest on your host, consistent with how the other four assistants already persist theirs; the same directory also holds prompt-history.jsonl, which records what you typed.
- A LAN endpoint (192.168.x.x) is blocked under network_egress_policy=restrict-local unless listed in network-allow.conf; public endpoints are reachable.
- A malformed layer-2/3 file stops OpenCode at startup with its own error — Nyia does not validate it.
- Threat note: a repository's opencode.json runs with your keys inside the box: it can add mcp servers (local commands started with the CLI), plugins, permission rules and providers with their own baseURL. Nyia runs a best-effort key-name scan and prints one warning when a project file appears to declare any of those (a determined repository can hide a key); the review is yours before launching a repository that is not yours (see THREAT_MODEL.md).
Version, channel and image selection¶
| Knob | What it does |
|---|---|
~/.config/nyiakeeper/VERSION |
the installed dist version (written by the installer / nyia update) |
~/.config/nyiakeeper/CHANNEL |
the update channel: latest (stable), beta, alpha (frozen). Switch with nyia update install <channel> |
NYIA_CHANNEL |
overrides the channel for one command |
NYIA_IMAGE_TAG |
pins the container image tag for one command, bypassing the channel — e.g. NYIA_IMAGE_TAG=v0.1.0-beta.7 nyia-claude. Works while that pinned tag is retained (newest 4 per channel + every stable) |
By default the launcher pulls the channel's floating tag on every launch, so a channel that moves — forward on release, backward on a rollback — reaches you at the next start.