Configuration
Most settings are managed from the terminal UI or the Companion app. Edit YAML when you want settings shared across projects or checked into a repository.
Quick setup
Open the provider picker in the terminal and choose Add Provider, or use Settings → Providers → Add provider in the Companion app. Sign in, then pick a model.
For API keys, local models and custom endpoints, see Providers and models. For project instructions, see Project instructions.
Configuration files
Files are read in this order. Later files override earlier ones; nested maps merge, scalars and lists are replaced.
| File | Purpose |
|---|---|
~/.vis/config.yml |
Your global settings |
~/.vis/state.yml |
Settings and credentials written by Vis; manage them from the UI |
<project>/vis.yml |
Settings shared with the project |
<project>/.vis/config.yml |
Local project overrides, usually gitignored |
Global configuration in ~/.vis accepts config.yml, config.yaml,
vis.yml or vis.yaml. The project root accepts vis.yml or vis.yaml;
<project>/.vis accepts config.yml or config.yaml. The project is the
directory where you start vis-agent.
Keys are snake_case strings; boolean keys start with is_. Unknown keys are
rejected, and an invalid file prints every offending path and exits with status
2 instead of starting:
Invalid Vis configuration in /project/.vis/config.yml:
- grep.include-gitignored-paths: unknown key (config is closed) — did you mean "grep.include_gitignored_paths"?
- mcp.servers.docs.transport: value rejected by the transport contract
Model names are free-form and are not validated; a wrong one fails at the provider.
A small config:
# vis.yml
system_prompt: Prefer restructuredText docstrings.
router:
budget:
max_cost: 5.0
environment:
ANTHROPIC_API_KEY: {env: ANTHROPIC_API_KEY}
Providers and models
The UI provides presets and sign-in for OpenAI, Anthropic (API and coding plan), OpenAI Codex, GitHub Copilot, Z.AI, Ollama and LM Studio. You can also configure endpoints that implement a supported API format, including local models:
providers:
- id: anthropic
api_key: ${ANTHROPIC_API_KEY}
models:
- name: claude-sonnet-4-5-20250929
- id: my-gateway
compatibility: openai # endpoint's API format
base_url: https://gateway.example.com/v1
api_key: ${LLM_TOKEN}
models:
- name: qwen3-coder-30b
context: 262144 # input window
output_limit: 32768 # max output tokens
is_tool_call: true
- id: my-responses-gateway
compatibility: openai-responses
base_url: https://gateway.example.com/v1
api_key: ${GATEWAY_TOKEN}
responses_path: /responses # only when served off another path
is_stateless: true # load-balanced replicas
models:
- name: gpt-5.6
Provider keys: compatibility, base_url, api_key, api_key_command,
responses_path, api_style, llm_headers, extra_body, is_stateless,
is_image_input. Providers with managed sign-in (Copilot, coding plans) need
no api_key.
Model keys: context, output_limit, is_tool_call, api_style. Filling in
the limits makes context checks and output capping accurate.
Models are offered in the order you list them; models discovered from the provider are appended after them.
API format
compatibility selects the endpoint's API format. api_style is the router's
name for the same setting and takes precedence when both are set. Use a
per-model value only when models on one endpoint use different APIs.
| Format | Request | Aliases |
|---|---|---|
anthropic |
{base_url}/messages |
claude, anthropic-messages, messages |
openai |
{base_url}/chat/completions |
openai-chat, openai-compatible, chat, chat-completions |
openai-responses |
{base_url} + responses_path |
responses, openai-compatible-responses |
gemini |
Gemini generateContent |
google, google-gemini |
Case, _ and - are normalised. An unknown value is rejected when the config
loads. A responses_path without a format implies openai-responses.
Declare the API you actually use. A gateway that serves both chat completions and Responses accepts either, but tool-call ids from one are rejected by the other and the turn fails with a 400.
Default and fallback
Picking a model in the UI writes two keys:
default_provider: zai-coding-plan
default_model: glm-5.2 # or one line: zai-coding-plan/glm-5.2
- There is one default pair for the whole config, not one per provider.
default_modelis looked up in that provider's catalog; an unknown name falls back to the provider's first model.- Without a default, the first provider and its first model are used.
Set a fallback provider and model for rate limits and provider failures:
fallback_provider: anthropic-coding-plan # must differ from default_provider
fallback_model: claude-sonnet-5
The fallback provider is tried right after the default; other providers follow in configured order. Logging out of a provider clears its pair.
Both pairs are per user. They are ignored with a warning in a committed
<project>/vis.yml; put them in ~/.vis/config.yml, ~/.vis/state.yml or the
gitignored <project>/.vis/config.yml.
On the command line, --model provider/model selects both for one run without
saving anything. The provider does not have to be configured if it has a
built-in preset with managed sign-in:
vis-agent --model zai-coding-plan/glm-5.2 "task"
vis-agent --model glm-5.2 "task" # on the active provider
Environment references
Any string value may use ${NAME}; $NAME is not recognised. Map keys are
not interpolated.
An unset variable does not fail the load. The provider manager and
vis-agent doctor report the provider as unusable, and the router skips it.
Selecting it explicitly returns an error. When Vis saves config, a
whole-value reference remains ${NAME}, not the resolved secret.
Images
Images produced in a session — matplotlib figures and image/* attachments —
are replayed to the model only when its name is known to support vision. A
text-only or unknown model gets the text results and no image block.
GitHub Copilot
Vis sends X-Initiator: user on the first call of each turn and
X-Initiator: agent on tool-call continuations and internal calls such as
session titling. Copilot determines billing; these headers do not guarantee a
particular charge. Setting X-Initiator in llm_headers overrides this
behavior. Claude models on Copilot are capped at balanced reasoning, and
trivial messages are sent without a reasoning parameter.
Evaluation runs
--reasoning-effort high|max sends the provider's exact effort value instead of
Vis's adaptive levels. The run exits 2 if the provider, model or value is not
accepted, or if any iteration switched provider or model; the JSON output
includes an eval object describing the run.
To add another provider, see Provider extensions.
System prompt
Add to the built-in prompt, or replace it:
system_prompt: Prefer restructuredText docstrings. Do not edit generated/.
system_prompt:
text: You are …
is_replace: true
.vis/SYSTEM.md and .vis/APPEND_SYSTEM.md in the project or ~/.vis do the
same from files and take precedence over these keys. Repository conventions
belong in AGENTS.md, not here. See
Project instructions.
Environment
The project's .env and .env.local are loaded automatically for processes
Vis starts: shells, REPLs, test runners and extensions. .env takes precedence
over .env.local. The parser accepts NAME=value, export NAME=value,
quotes and comments.
environment: declares variables a dotenv file cannot, and never holds a
secret value itself:
environment:
OPENAI_API_KEY: {env: WORK_OPENAI_KEY} # another process variable
STRIPE_KEY: {dotenv: STRIPE_TEST_KEY} # a dotenv entry under a new name
EXA_API_KEY:
keychain: vis-exa # macOS Keychain or secret-tool
account: alice # optional
GITHUB_TOKEN:
command: [gh, auth, token] # trimmed stdout is the value
VIS_MANAGED: {literal: "true"} # non-secret marker
Exactly one source per entry. A declared name never falls back to .env or the
ambient environment, and a blank value means unset. literal requires the
wrapper and is refused for credential-looking names (*_KEY, *_TOKEN,
*_SECRET, *_PASSWORD). Command and keychain values are fetched without a
shell, cached briefly and never logged.
Resolution order everywhere: environment: → .env, .env.local → the
environment Vis was started from.
With the jail enabled, the parent process environment is excluded.
{env: NAME} explicitly includes a variable, while jail.environment: inherit
includes the full environment. LD_*, DYLD_*, PERL* and BASH_ENV are
always refused.
A shell or REPL call can add or override variables. Literal values are recorded in the transcript, so use a source reference for secrets:
sh = await shell("npm test", {"env": {"NODE_ENV": "test"}})
r = await repl_start({"language": "python",
"env": {"STRIPE_KEY": {"keychain": "vis-stripe"}}})
Vis refuses to reuse a running REPL with different env values. Stop it before
starting one with a different environment.
Router
Configure retry delays, network timeouts and spending limits. Omit this block to use defaults.
router:
rate_limit:
same_provider_delays_ms: [2000, 3000, 6000]
is_respect_retry_after: true
is_fallback_provider: true # may a rate-limited turn move to another provider
network:
timeout_ms: 300000
idle_timeout_ms: 45000
budget:
max_tokens: 1000000
max_cost: 5.0
Jail, filesystem and network
The jail is off by default. Enable it whenever the model runs untrusted code;
without it, shells and language processes run with your full permissions. With
jail.enabled: true, commands run under Seatbelt (macOS) or bubblewrap (Linux)
and through the gateway's egress proxy. Unsupported hosts return an error.
Declare directories in workspace.filesystem, then allow them by id in
jail.filesystem.allow. The jail does not expose unlisted roots.
| Key | Meaning |
|---|---|
id |
Name used by the allow list and the UI |
path |
Absolute or ~-relative directory |
description |
Optional; what the model is told the root is for |
python_name |
Optional Python variable for the path, e.g. runtime_path |
access |
read-write or read-only |
search |
Whether search indexes it |
draft |
shared, copy-only, copy-and-apply or not-allowed in an isolated session (see Drafts) |
when, optional |
Mount only on some hosts or when the path exists |
Allowed, searchable roots have a Python Path variable named after the
directory (vis-python-runtime → vis_python_runtime_path). Set python_name
to choose another name. project_root_path is the current project. Access to
~/.vis is always allowed.
# vis.yml
workspace:
filesystem:
- id: sibling
path: ~/sibling-repository
draft: copy-and-apply
- id: reference
path: ~/shared-reference
access: read-only
- id: m2
path: ~/.m2
description: Maven/Clojure dependency cache
search: false
- id: cuda
path: /usr/local/cuda
when:
exists: /usr/local/cuda
- id: scratch
path: ~/scratch
optional: true
jail:
enabled: true
environment: declared # or inherit
filesystem:
allow: [sibling, reference, m2, cuda, scratch]
keychain: true # let gh/git credential helpers reach the OS keychain
network:
allowed_domains:
- github.com
- npmjs.org
denied_domains:
- example.invalid
allow_private: false
inbound_ports: # ports a confined server may listen on
- 5273
when.os accepts macos, linux, wsl or windows. A missing admitted path
is reported by vis-agent doctor.
repl_connect attaches to an existing process, which Vis cannot jail. Processes
Vis starts are jailed when jail.enabled is true.
Process jail and network policy explains the policy in full, including
network rules and how to diagnose a refusal. If a native tool such as bb or
clj-kondo fails with CSunMiscSignal.open() failed after an upgrade, restart
Vis: the jail profile is inherited by running processes.
Python installs packages in ~/.vis/python/packages and writes bytecode caches
in ~/.vis/python/pycache. Override these locations with VIS_PYTHON_PACKAGES
and VIS_PYTHON_PYCACHE_PREFIX. VIS_PYTHON_HOME and VIS_PYTHON_NATIVE_PATH
select another runtime. All four variables are read at startup.
Python package index
Set the embedded runtime's primary package index in vis.yml:
python:
index_url: https://gateway.example.com/simple
This setting applies to Vis-managed pip installs and to extension projects selected
with tool.vis.project. Vis passes it as uv's
--default-index; named indexes and [tool.uv.sources] retain uv semantics.
It is read from merged configuration for each install. It does not change a
project's own .venv or configure uv commands run outside Vis.
index_url overrides pip's primary index from PIP_INDEX_URL or pip.conf.
When absent, each installer keeps its inherited settings. Other pip settings,
including extra indexes, proxies and certificates, remain unchanged. Prefer one
company virtual index serving both private and public packages; extra indexes
are not ordered fallback sources and can introduce dependency confusion.
Use a literal HTTP(S) URL without credentials, a query string or a fragment.
Keep authentication outside committed YAML, for example in the gateway user's
.netrc. Invalid index values stop installation rather than falling back to a
public index. Existing installed packages are not reinstalled by this setting.
Python import roots
vis-agent python puts the project's packages on sys.path, so
vis-agent python -m pytest tests/ imports a src/ layout without
PYTHONPATH. Roots are read from pyproject.toml (setuptools, pdm, poetry,
hatch, pytest pythonpath), setup.cfg, pytest.ini and tox.ini. No roots
are inferred without this metadata. To declare roots explicitly:
# vis.yml
python:
source_paths: [src, lib/vendor, ~/shared/py]
runner: project # default run_tests backend: project | vispython
Configured paths come first, then inferred ones; PYTHONPATH precedes both.
An editable package install supplies its own import
roots through .pth files or backend hooks; it does not need these layout overrides.
Import roots do not grant filesystem permissions or install dependencies.
runner: project runs the project's own pytest with its installed
dependencies; vispython runs in the embedded sandbox.
An explicit runner argument on the call
(run_tests({"language": "python", "runner": "project"})) overrides this
default for one run.
The project interpreter is chosen automatically: uv, then Poetry, then .venv
or venv, then python3.
MCP servers
Servers you add from the UI, the API or vis-agent gateway mcp are written to
~/.vis/state.yml. Servers declared by hand elsewhere are used too, but the UI
cannot edit them; change the file that declares them.
The gateway keeps one connection per enabled server, shared by every session,
and reconnects a crashed one. Kill closes the connection until Start or
a gateway restart; enabled: false persists.
The gateway starts an OAuth flow when an HTTP MCP server requests it with
401. From the terminal, use MCP Servers; from the CLI:
vis-agent gateway mcp add linear --url https://mcp.linear.app/mcp
vis-agent gateway mcp auth-start linear
# open the printed URL, approve, then paste the redirect URL or code:
vis-agent gateway mcp auth-complete linear --flow-id <FLOW_ID> --input "<URL_OR_CODE>"
vis-agent gateway mcp list
A static token skips sign-in:
vis-agent gateway mcp add linear --url https://mcp.linear.app/mcp \
--headers "Authorization=Bearer <TOKEN>"
Other verbs: test (connect without saving), remove, enable, disable,
kill, start, auth-poll, auth-cancel, auth-logout. Saving a server
without env or headers keeps the stored values.
Feature toggles
toggles:
shell: false # default true; removes shell(...) from the sandbox
introspection: true # default false; lets the agent read its own session data
council: true # default true; project log and explicit active-session pings
draft_backend: auto # auto | worktree | rift | off; how the agent isolates a draft (see drafts.md)
Run /reload after editing.
Session titling
A session is named from its first message immediately, then improved by one short model call after the turn finishes.
titling:
mode: llm # llm | first_sentence | first_words | disabled
provider: zai-coding-plan # optional: pin the title call
model: glm-4.7
Database
Sessions are stored in SQLite. Resolution order: --db flag, VIS_DB_PATH,
db_spec, then ~/.vis/vis.mdb. --db :memory uses an in-memory database.
db_spec:
backend: sqlite
path: /somewhere/else/vis.db
Grep
Search always honours .gitignore. To search a gitignored subtree such as
vendored repositories, re-include it here:
# vis.yml
grep:
include_gitignored_paths: [repositories/]
Both lists use .gitignore pattern syntax. Omit always_exclude to use the
defaults: .git/, node_modules/, target/, build/, dist/, __pycache__/,
.venv/, .gradle/, vendor/, .next/, out/, .m2/, .shadow-cljs/,
cljs-runtime/, .cpcache/, .clj-kondo/, .calva/, .lsp/ and .rift/.
Setting always_exclude replaces, rather than extends, that list.
Run /reload after editing.
See also
- Process jail and network policy — the
jailblock in full. - Project instructions — AGENTS.md, SYSTEM.md and prompt templates.
- Extending Vis — configuring providers, tools and toggles.
- Remote access and the Companion app — gateway keys and tokens.
