Vis

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.

When to use

  • You want to use a model from OpenAI, Ollama, another provider or your own endpoint, or set its API key. Add it from the app in Quick setup, or declare it in YAML under Providers and models.
  • Your team should share the same settings. Commit a project vis.yml. Configuration files shows which file wins when several set the same key.
  • One session must not use your global or project setup, or some extensions. Start it with the flags in Leave out global or project configuration.
  • One session needs only some tools of an extension, such as py.lint_code but not py.repl_start. Start it with the flag in Choose single tools.
  • Some tools must stay off in every session of a project or group. Set the tool lists in Turn tools off for a project, group or session.
  • One session or group needs different behavior. Open its scoped settings instead of changing the gateway defaults.
  • A provider hits rate limits or fails, or a task should stay within a budget. Set a fallback model, and set retries and token and cost limits under Router.
  • New sessions think too much or too little on one provider. Set its default thinking level.
  • Commands that Vis runs need your project's environment variables. Put them in .env, as described in Environment.
  • Vis should use tools from an MCP server. Add the server as described in MCP servers.
  • Search skips a directory that .gitignore excludes, such as vendored repositories. Include it again under Grep.
  • Python HTTPS requests fail a strict certificate check, or packages must come from a private index. See Python TLS validation and Python package index.
  • Your project installs its Python dependencies only into shared packages. Let its extensions use them too with Python extension environment.
  • Your own program must read or change settings. Read Configuration API.

Use Project instructions for rules about working in your codebase, and the process jail to limit what commands can access.

Quick setup

In the terminal, open the provider picker and choose Add Provider. In the Companion app, use Settings → Providers → Add provider. Sign in, then pick a model.

For API keys, local models and custom endpoints, see Providers and models. For project instructions, see Project instructions.

Coding-agent name

Set agent_name in the project's vis.yml:

agent_name: Ada

The default name is Vis. You can also change it in Settings → General → Agent name in the TUI, or Settings → your gateway → General in Companion. Saving in Settings writes to the gateway's ~/.vis/state.yml, not the client's disk. This gateway-wide name overrides project names. Remove the key from state.yml to use project defaults again.

A name can have up to 80 characters. It cannot be blank or contain control characters. Vis removes spaces at the start and end.

A change in Settings updates open sessions immediately, also on remote clients. If you edit the YAML file by hand, reopen the session to show the new name. The name changes the next default system prompt. It does not change product branding, session titles or a full custom prompt, which keeps its own identity.

Programs read and save the name like any other setting, with the Configuration API. An invalid name returns 400 without changing the saved value. The settings list shows the name as a string row in the general group. Session details include the resolved agent_name, so reconnecting clients receive the current value.

Configuration files

Files are read in this order. Later files override earlier ones. Nested maps merge, scalars and lists are replaced. The gateway-wide agent_name saved in state.yml is an exception: it overrides the project tiers. extensions merges by package name, replacing each complete declaration rather than mixing source and version fields from different files. See declarative extension packages for global/project installation scopes and vis-agent extension sync --trust.

File Purpose
~/.vis/config.yml Your global settings
~/.vis/state.yml Settings and credentials that Vis writes. Change them in 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. The <project>/.vis directory accepts config.yml or config.yaml. The project is the directory where you start vis-agent.

Keys are snake_case strings. Most boolean keys start with is_, but some do not, for example enabled and tls_strict. Vis refuses unknown keys. An invalid file prints each wrong path, and Vis 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}

Leave out global or project configuration

Start the terminal or a one-shot run with a flag when a session must not use part of your setup:

vis-agent tui --no-global                 # only the project configuration
vis-agent tui --no-project                # only your global configuration
vis-agent tui --vanilla                   # neither: plain Vis with your providers
vis-agent tui --extensions gh,clj         # only these extensions
vis-agent tui --extensions -spel,-uplink  # every extension except these
vis-agent tui --extensions none           # no optional extension
Flag What the session leaves out
--no-global ~/.vis/config.yml, the settings in ~/.vis/state.yml, ~/.vis/extensions/, ~/.vis/AGENTS.md and global skills
--no-project vis.yml, .vis/config.yml, .vis/extensions/, AGENTS.md files and .vis/skills
--vanilla Both of the rows above

Providers, sign-in data and your default and fallback models stay in every case. Without them, a session has no model.

The same flags work for a one-shot run, also with --json-schema:

vis-agent --vanilla --json-schema @city.json "Name the capital of Poland"
vis-agent --no-global --extensions -spel --persist "Fix the failing test"

In the terminal, the flags apply to each new session that it opens. A saved session keeps the choice that it started with. Do not combine the flags with --session-id, --resume or --continue.

An extension name is the name in its manifest. A list of names keeps only those extensions. A list of -names turns only those off. Do not mix the two forms in one list. Vis refuses an unknown name before the session starts. Parts of Vis without an Auto/On/Off setting always stay on.

For scripts, set VIS_SOURCES to global, project or none, and set VIS_EXTENSIONS to an extension list. A flag wins over its variable.

A session cannot change the files of a tier that it leaves out. Vis refuses the change with a 409 error. To start such a session from a program, see Sessions API.

Choose single tools

Use --tools when a session needs an extension, but not all of its tools:

vis-agent tui --tools py.lint_code,py.repl_eval   # only these two tools of py
vis-agent tui --tools -py.repl_stop               # every tool except py.repl_stop
vis-agent tui --tools 'py.repl_*'                 # only the REPL tools of py

A tool name is the name that the agent calls, such as py.lint_code. A list of names keeps only those tools of the extensions that they name. Other extensions keep all of their tools. A list of -names turns only those tools off. Do not mix the two forms in one list. A * matches any part of a name, as in py.repl_*.

Vis refuses an unknown name before the session starts, and the error lists the tools of that extension. For scripts, set VIS_TOOLS to a tool list for new sessions. The --tools flag wins over the variable.

Change the tools of a saved session

A saved session keeps its tool list when you resume it. To change the list, give --tools again when you open the session:

vis-agent --session-id 6d78b3f2 --tools -py.repl_stop "Run the tests"
vis-agent tui --continue --tools py.lint_code,py.repl_eval
vis-agent tui --resume --tools '*'                # every tool on again

The new list replaces the saved list, and the session keeps it for later resumes. In the terminal, the list applies to the first saved session that you open. That is the session of --session-id or --continue, or the session that you pick after --resume. VIS_TOOLS never changes a saved session. Programs can change the list at any time, also during a turn, as shown in Sessions API. A busy session gets the new list at its next step.

The choice applies only to tools of extensions with an Auto/On/Off setting. A tool that is off is not in the session's Python sandbox. The extension's prompt text can still name it.

Turn tools off for a project, group or session

Use tool lists in configuration when the same tools must stay off in every session of a scope. Add a tools section to a global or project file:

# vis.yml
tools:
  deny:
    - py.repl_*          # no Python REPL tools
  allow:
    - clj.lint_code      # of clj, keep only these two tools
    - clj.run_tests

The entries use the names and the * of --tools. An allow list keeps only the named tools of the extensions that it names. A tool that a later release adds to such an extension also stays off. A deny list turns only the named tools off.

You can also set the lists in the Allowed tools and Turned-off tools rows of global, project, group and session settings. The rows suggest the tool names of the session's extensions. Vis refuses a name that matches no tool, and it writes nothing.

Every list applies: from the configuration files, from each settings scope and from --tools. A more specific scope cannot turn on a tool that a wider scope turns off. A session that another session starts also gets the lists of that session.

A tool that is off is not in the sandbox, and Vis refuses calls to it, also through a saved handle. New and resumed sessions get the lists at their start. A change during a session applies at its next tool call.

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_calls_supported: 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_vision_supported. Providers with managed sign-in (Copilot, coding plans) need no api_key.

Model keys: context, output_limit, is_tool_calls_supported, is_vision_supported, api_style. Filling in the limits makes context checks and output capping accurate.

Vis also reads the old names is_tool_call and is_image_input. When a file has the old name and the new name, the new name wins. Vis writes only the new names.

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 that you actually use. A gateway that serves both chat completions and Responses accepts either API. But each API rejects tool-call IDs from the other, and the turn fails with a 400 error.

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_model is 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

Default thinking level

Each provider can have its own default thinking level. Sessions on that provider start with it. To set it, open the provider's menu:

  • In the app, open Settings, then the provider's row actions, and choose Default thinking.
  • In the terminal, open the provider list and choose Set Thinking Level....

The menu lists quick, balanced and deep. With Simplified thinking modes off, it lists the exact levels that the provider's models offer. Choose Use the built-in level to remove the default. Vis then uses its built-in level. The choice goes to the provider's entry:

providers:
  - id: anthropic
    reasoning_level: deep       # quick, balanced or deep
  - id: openai
    reasoning_effort: high      # an exact level, used when simplified modes are off

The reasoning control in a session footer changes only that session. Global toggles.reasoning_level and toggles.reasoning_effort keys have no effect.

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

Attached pictures, matplotlib figures and other image/* attachments go to the model only when it reads images. The model catalog decides this for known models. Set is_vision_supported on a model to decide it yourself:

providers:
  - id: my-gateway
    compatibility: openai
    base_url: https://gateway.example.com/v1
    models:
      - name: qwen2.5-vl-72b
        is_vision_supported: true     # reads images
      - name: qwen3-coder-30b
        is_vision_supported: false    # gets a text description of each picture
  • true: Vis sends the pictures. Use it for a local or private model that the catalog does not know.
  • false: Vis sends a text description of each picture.
  • Not set: the model catalog decides.

is_vision_supported: false on a provider blocks images for all its models, also when a model says true. A provider extension sets the same value with ProviderModel(is_vision_supported=...).

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. Trivial messages to Claude models on Copilot, such as a greeting or a thank-you, are sent without a reasoning parameter.

Evaluation runs

--reasoning-effort LEVEL sends one exact thinking level that the model offers, such as high or max, 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. With the jail disabled, child processes keep the full environment. LD_*, DYLD_*, PERL* and BASH_ENV are always refused. Child processes never inherit NODE_OPTIONS from the environment Vis was started from. Declare it under environment: if a project needs it.

A shell 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"}})
sh = await shell("./deploy.sh", {"env": {"STRIPE_KEY": {"keychain": "vis-stripe"}}})

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. To keep specific files out of every grant, list patterns under jail.filesystem.deny_read or jail.filesystem.deny_write. The file tools of Vis obey these rules whether the jail is on or off. Only jail.enabled: true keeps child processes out. See Deny specific files.

Key Meaning
id Name used by the allow list and the UI
path Absolute or ~-relative directory
description Optional. Tells the model what the root is for.
python_name Optional Python variable for the path, for example 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.

You can ask Vis to create one draft across several added read/write repositories, even when their policies are shared. draft_create("task", roots=[project_root_path, sibling_path]) selects participants without changing the catalog. The first root is primary. See Work in another repository.

# vis.yml
workspace:
  filesystem:
    - id: sibling
      path: ~/sibling-repository
      draft: copy-and-apply
    - id: reference
      path: ~/shared-reference
      access: read-only
    - id: npm
      path: ~/.npm
      description: Node build 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]
    deny_read:                   # patterns denied to every jailed reader
      - .env
      - "**/.env*"
    deny_write:
      - deploy/production
  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.

A tool that attaches to an already running process cannot jail it. Processes Vis starts are jailed when jail.enabled is true.

Process jail and network policy explains the full policy, including network rules and how to diagnose a refusal. Running processes keep the jail profile that they started with. So if a native tool such as bb or clj-kondo fails with CSunMiscSignal.open() failed after an upgrade, restart Vis.

Shared Python installs use ~/.vis/python/packages. Project installs stay in their uv environment. VIS_PYTHON_PACKAGES overrides only the shared location. Bytecode caches use ~/.vis/python/pycache, overridden by VIS_PYTHON_PYCACHE_PREFIX. VIS_PYTHON_HOME and VIS_PYTHON_NATIVE_PATH select another runtime. These variables are read at startup.

Python TLS validation

Both ~/.vis/config.yml and project vis.yml accept this setting, using the normal configuration merge order:

python:
  tls_strict: true

The default is true, which keeps Python's TLS validation. Set it to false only for a trusted corporate CA that fails strict X.509 checks. For example, a CA fails these checks when its Basic Constraints extension is not marked critical:

python:
  tls_strict: false

This clears only ssl.VERIFY_X509_STRICT when Python SSL contexts receive their verification flags. Trust-chain, certificate-signature, expiry and hostname verification remain enabled. It does not add trusted certificates or retry a failed handshake with verification disabled. STRICT covers multiple X.509 requirements. Disabling it is a security trade-off, not a certificate repair. Prefer a correctly issued CA when your administrator can provide one.

The same merged value applies to python_execution, trusted Python extensions and vis-agent python, with or without --shared. Language extension REPLs that run vis-agent python get it too. It affects Python's ssl contexts, including contexts created by stdlib clients and libraries that use ssl.SSLContext. It does not affect JVM TLS, gh, curl, uv, external Python processes or libraries using a different TLS implementation.

Vis reads the value when a worker starts and each time vis-agent python starts. Vis also sets VIS_PYTHON_TLS_STRICT to true or false in the environment of that Python program. After you change the value, run /reload to rebuild session workers. If the gateway already runs a worker that registers extensions, restart Vis. Existing connections do not change. Use a YAML boolean: false, not the string "false".

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, vis-agent python uv, and automatic extension project preparation. It is read from merged configuration for each subprocess. For uv, Vis supplies UV_DEFAULT_INDEX when neither UV_DEFAULT_INDEX nor UV_INDEX_URL is already set. Use --default-index for a command-line override. uv's deprecated --index-url and -i do not override UV_DEFAULT_INDEX. Named indexes and package source pins remain managed by uv.

index_url overrides pip's primary index from PIP_INDEX_URL or pip.conf. When absent, both installers keep their inherited settings. Other installer 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

After vis-agent python uv sync --project ., run your package from the same project directory with vis-agent python -m your_package. The standalone CLI selects one dependency environment before Python starts:

  • A current-directory .venv, pyproject.toml, or UV_PROJECT_ENVIRONMENT selects project mode. Only that environment supplies installed packages, distribution metadata and editable .pth files or import hooks. Shared Vis packages and their startup hooks are not loaded.
  • Without a project, the CLI uses shared packages in ~/.vis/python/packages (or VIS_PYTHON_PACKAGES).
  • vis-agent python --shared -m your_tool explicitly selects shared packages, even inside a project. It skips project activation and configured or inferred source roots. An explicit PYTHONPATH still applies.

UV_PROJECT_ENVIRONMENT selects another project environment. Relative paths resolve against the current directory. The CLI does not search parent directories, create an environment or sync dependencies on startup. A pyproject.toml with no prepared environment reports a sync error instead of borrowing shared packages.

The CLI still uses Vis's embedded Python, so the environment must match its Python version and platform. If the matching site-packages directory is missing, the CLI reports a diagnostic rather than silently falling back to shared packages. To use the project's own interpreter instead, run vis-agent python uv run --no-sync python -m your_package.

Environment activation happens inside the sandbox: editable source and custom environments still need to be within allowed filesystem roots. Only activate environments you trust. Editable import hooks can execute code. This standalone CLI behavior does not add the project's environment to agent python_execution.

vis-agent python also puts declared source roots 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]

Configured paths come first, then inferred ones. PYTHONPATH comes before both. All these roots come before the packages of the selected environment. Vis does not merge project and shared packages. An editable package install supplies its own import roots through .pth files or backend hooks, so it does not need these layout overrides. Import roots do not grant filesystem permissions or install dependencies.

Python extension environment

By default, each uv project of a project extension gets its own environment, normally .venv. If your project installs its dependencies with vis-agent python --shared uv sync, use the shared packages for its extensions too:

# vis.yml
python:
  environment: shared

The value is project (the default) or shared. With shared, Vis prepares the extension projects inside the project directory like vis-agent python --shared uv sync, and their workers import ~/.vis/python/packages. Vis does not create or read .venv for them. Installed extension packages and global extensions keep their own environments. Run /reload after you change the value. See Use shared packages for project extensions.

The Python language tools also follow this value. With shared, py.run_tests and py.repl_start run project Python with vis-agent python --shared, and they do not use the project's .venv. Extensions read the value with vis.python_environment().

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 for each enabled server, which all sessions share. It reconnects a connection that crashed. Kill closes the connection until you press Start or the gateway restarts. To keep a server off after a restart, set enabled: false.

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; classified project messages, replies and explicit pings
  draft_backend: off    # default off; opt in with auto | worktree | rift (see drafts.md)
  automations: true     # default false; lets schedules and webhooks start turns (see automations.md)

Run /reload after editing.

Machine settings

Settings for one machine have six sections:

  • General: the agent name, Council and its rooms, notifications and experimental features.
  • Providers: provider accounts and options for model requests, such as automatic fallback.
  • Voice: speech engines, the voice model and the transcription of recordings.
  • Permissions: files, network, process access and sandbox tools.
  • Tools: MCP servers and extensions.
  • Automations: prompts that run on a schedule or a webhook.

The TUI shows the same sections. Notifications, speech engines and automations are only in the Companion app.

In the TUI, the Providers header has an Add provider button. Below it, Configured providers lists your accounts and Configuration holds the provider options. MCP servers have their own MCP servers section after Tools, with an Add button in its header.

Project, group and session settings

In the app, select Settings with the cog icon in a session's … menu. For a project or a group, select Settings in its actions. In the TUI, use Session settings, Group settings or Project settings from the command palette. The settings rows show the effective value and where it comes from. Use inherited value removes only the override at the scope you opened.

Vis resolves each setting in this order: global → project → group → session. A scope with no value is skipped. false is an explicit value, not inheritance. For example, turn a setting off in a group and turn it on in one session. To make that session follow the group again, choose Use inherited value in it. A change to the group does not overwrite an explicit choice in another session.

A more specific value wins. When a more specific scope decides a row for the open session, Settings locks that row. The row names the scope that decides it.

In this example, your project's vis.yml sets toggles.shell: false. While a session from that project is open, the global Shell commands row is locked in TUI Settings and in the app. Turning it on globally does not change that session. Open Project settings and turn it on there. Vis writes the change to .vis/config.yml, which overrides vis.yml without editing it. From a session outside that project, you can still edit the global row.

Global means this gateway, which all its connected clients share. Project settings use the canonical project root, not the working-copy path of a draft. Edits go to the project's .vis/config.yml, and the checked-in vis.yml does not change.

The gateway database keeps group and session overrides. A session that you move keeps its own values and follows its new ancestors. New sessions and forks inherit from their ancestors, not the overrides of the source session. Sessions without a group skip the group layer.

Response options, including reasoning, verbosity, thinking summary and fast mode, are captured when you submit a message. Later edits do not change running or queued responses.

With Simplified thinking modes on, the default, the reasoning control switches to the next of quick, balanced and deep with each tap or key press. Turn the setting off to choose from a list of every thinking level that the current model offers. The setting is in the Application section of the app's Settings, and in the top section of the terminal's Settings. Vis saves that choice as reasoning_effort. After a model change, each turn sends the nearest level that the new model offers.

The reasoning control changes only the current session. A new session starts with the default thinking level of its provider.

Paths and access rows use guided editors. Changes apply on the next turn. To edit configuration files directly, use a text editor outside the app or TUI.

Local permissions cannot expand the host's global access policy. Draft settings govern future operations. Changing them does not move or delete an existing draft.

Skill and MCP availability applies to the next lookup or call, including a call by an already-known name. An ongoing external call can finish. Disable a skill globally or locally to remove it from discovery, doc(), prompt inventories and slash entry points. This does not erase text already read or deny filesystem access.

Optional tool extensions have Auto, On and Off engine settings. Auto uses the extension's existing applicability check. On keeps it active. Off hides its tools and rejects new calls, including saved handles. This is extension activation, not model routing. Core, provider and channel infrastructure is not switchable here.

Provider setup, credentials, OAuth and shared MCP start/stop operations stay global. Theme and other device preferences remain local to the app or terminal. Only settings declared for the selected scope appear in its catalog, with one exception.

An extension setting that a wider scope decides shows read-only. Its row names that scope and opens its settings, for example Machine settings. Machine settings also name the project extensions of the current session. Reload them in Project settings.

Scoped MCP servers

Use the MCP panel in the same scoped dialog to add a server definition or control its availability. Local definitions can shadow an inherited name without editing its parent. Use inherited removes only the local definition. Local definitions cannot store environment credentials, HTTP headers or authentication settings. Disabling availability for one session does not stop a shared connection. Enabling availability does not start a server disabled by the global administrator.

The CLI accepts the same explicit scope and target for list, add, remove, enable and disable:

vis-agent gateway mcp list --scope session --target-id <session-id>
vis-agent gateway mcp add docs --url https://gateway.example.com/mcp --scope group --target-id <group-id>
vis-agent gateway mcp disable docs --scope session --target-id <session-id>
vis-agent gateway mcp remove docs --scope group --target-id <group-id>

Omit both flags to manage global servers. Project targets accept the project ID or its canonical root. Group and session targets use their IDs. Authentication and lifecycle commands remain global-only.

Extension settings

The Extensions part of the Tools section holds the settings of all extensions. Each extension with settings has its own group under its name. The project tag marks an extension of the current project. The global tag marks a machine extension. Extensions that ship with Vis have no tag.

Opening Settings never runs extension code. After you add, change or remove an extension file, reload the extensions:

  • In the TUI, select Reload in the Extensions header line.
  • In the app, select the round arrows next to the Extensions heading.

Vis runs the extension files again, then reads the settings list again.

In global settings, the reload covers machine extensions. In project, group or session settings, it also covers the extensions of that project. Vis then shows each scope with its directory and how many extensions loaded and failed there. If a directory has no extensions, Vis says that it found none there. A reload from global settings also tells you to use project settings or /reload for project extensions.

If an extension fails to load, its group stays in the list and shows the error. If a reload fails after an earlier load, Vis keeps using the earlier version. The group then says so. Your stored values do not change when an extension fails, goes away or loads again.

Some extensions ask for a password or an access token, for example for a hosted service. Settings shows such a secret setting only as Saved or Not set, never its value:

  • To save or change the value, select Set or Replace, type the value and save.
  • To delete the saved value, select Remove and confirm.

Vis keeps a secret value in ~/.vis/state.yml on this computer and only in global settings. Only your user account can read that file. A project vis.yml cannot change the value. Saved trajectories show [REDACTED] in its place.

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

Gateway pairing address

The pairing link for the companion app has an address that Vis finds on this computer. Your network can need a different address, for example a port forward, a proxy or the one address that the network allows. Name that address here, and every pairing link puts it first:

gateway:
  advertise: 10.0.0.5

The value is a host, host:port or a full URL, and the detected addresses still follow in the same link as fallbacks. --advertise on vis-agent gateway start or vis-agent gateway pair wins over this key, and VIS_GATEWAY_ADVERTISE sits between the two. The config file is the source a gateway started by launchd or systemd can read, because a service unit starts without your shell profile. Setting it changes what the link says, not what the gateway listens on, so the address still has to reach this computer.

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. The defaults are .git/, node_modules/, target/, build/, dist/, __pycache__/, .venv/, .gradle/, vendor/, .next/, out/, .m2/, .shadow-cljs/, cljs-runtime/, .cpcache/, .clj-kondo/, .calva/, .lsp/ and .rift/. If you set always_exclude, it replaces that list. It does not add to it. Run /reload after you edit these lists.

See also