Skip to content

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)

  1. CLI flag / env
  2. Project private — .nyiakeeper/private/config/nyia.conf (hand-edit only)
  3. Project global — .nyiakeeper/nyia.conf (written by nyia config project)
  4. Project shared — .nyiakeeper/shared/config/nyia.conf (hand-edit only; committed downstream)
  5. Global + assistant — ~/.config/nyiakeeper/config/<assistant>.conf
  6. Global — ~/.config/nyiakeeper/config/nyia.conf (written by nyia 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 .gitignore again, 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 (or AGENTS.md, ...) line an older version put in Nyia's info/exclude block is removed once the old root link it covered is gone (the session removes that link). A bare, unmarked CLAUDE.md line 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/exclude block 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

  • .gitignore outranks info/exclude, so hiding only works once Nyia's rules are out of .gitignore. The first local launch writes the info/exclude block, checks it, then removes Nyia's marked block and the bare .nyiakeeper lines from .gitignore, and tells you to commit .gitignore. If git does not then name info/exclude as the file that ignores .nyiakeeper/private/, the old .gitignore is 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 --cached for you.
  • It is per clone, per person. Each clone needs it, and a teammate who launches Nyia without hide_footprint adds the .gitignore block back.
  • Only a real .git directory is written. In a linked worktree or a submodule (where .git is a file), or when .git is a link or redirected, Nyia writes no info/exclude and says so in local mode: a session can plant those to point at another repository on your machine. In local mode the .gitignore rules are then left in place, so nothing is hidden there.
  • It turns off team sharing. .nyiakeeper/shared/, exclusions.conf, workspace.conf and whatsup/ 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.md link into .nyiakeeper/ (from an older version) is a dangling link for everyone who clones: Nyia reports it and never removes a tracked file; git rm it. Your own tracked CLAUDE.md/AGENTS.md is 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 .git does not carry info/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), so view / project / global behave identically in both. nyia config get returns only infrastructure paths (home, config, data) — use nyia config view for 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 config keys (discoverable via nyia config view, scoped, validated).
  • Large or append-managed collections that have their own CLI → structural files (e.g. exclusions.conf via nyia exclusions). Heuristic, not absolute: NYIA_PROTECTED_BRANCHES is 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_cutoff key + nyia git-history ops; network_egress_policy key + 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.