Skip to content

Configuration

Attaché reads its settings from attache.toml in the home folder: ~/.attache/attache.toml, or the folder that --home or ATTACHE_HOME points to. attache init writes a commented starting file.

Editing in the web workspace

Open Settings in the web workspace to inspect the saved configuration and the configuration Attaché is currently running. The forms and Editable settings (TOML) view change the same supported fields: assistant name, model endpoints and routing, web settings, rate limits, and whether existing channels are enabled.

Review the changes before saving. Attaché validates the proposal, checks that the file has not changed since you loaded it, and saves it atomically with a previous-version backup. If another editor changed the file, reload Settings and review your changes again. A restore uses the same validation and conflict checks as a save.

The TOML view is an editable selection, not the complete source file. Comments, credentials, identity rules and other file-managed options are preserved in the original file. Secret values are never sent to the browser; endpoint credentials show their configured status. Create new endpoints or channels, or edit options outside the selection, in attache.toml. Some unusual TOML layouts cannot be changed safely in the browser; Settings will tell you to edit that field in the file instead.

Saving does not restart Attaché or change an active conversation. Settings indicates when a restart is needed. Stop and restart attache or attache run, then open the new launch link; if you installed the startup service, restart it using the service's normal controls.

Settings is available only in the owner's local interface. Generated pages and public share links cannot access it.

Managing the configuration file

After upgrading Attaché, run attache init to add new bundled skills; it never overwrites your files.

attache init also makes the home folder yours alone, an existing home included, so running it after an upgrade locks down a home made by an older Attaché too. On macOS and Linux that is chmod 700. On Windows it is an access list that lets in only you and SYSTEM, with inheritance off, so everyone else, Administrators included, loses access to everything in it. It does this only to a folder that is new, empty or already a home (it holds attache.toml). It leaves any other folder as it is and says so, in case --home named the wrong one.

Attaché reads the file once, when it starts, so restart attache or attache run after you change it. Run attache doctor to check your edits; a mistyped key is reported with the keys that section accepts:

$ attache run
attache.toml: /home/sam/.attache/attache.toml: unknown key prot in [web]; known: host, port

The file attache init writes

# Attaché settings. Restart `attache run` after editing.
# People and chats are keyed by platform ids, never by display names.

[assistant]
name = "Attaché"  # how the assistant names itself; also its trigger word in group chats

[owner]
name = "Sam"

# Your own sender id on each chat app, so your direct messages there reach you as the owner.
# Ids can also go straight in [owner] (or [people.<id>]), like slack = "U0123".
[owner.ids]
slack = ""

[routing]
own_chats = "claude"        # your own chat (terminal and web) and your direct messages
group_chats = "claude-api"  # group chats, and anyone but you

[web]
host = "127.0.0.1"  # only this machine can reach the web page
port = 8765
# Share links, for people off this machine, through Tailscale Funnel (docs/configuration.md).
# Off until public_url is set; then run `tailscale funnel --bg <public_port>`.
# public_url = "https://pc.tailnet.ts.net"  # https:// and your machine's name, no path
# public_port = 8766                        # serves share links only; default port + 1

# Models. api: anthropic | openai-chat | openai-responses | claude-cli. Other keys, such as
# max_tokens, are passed to the backend as options.
[endpoints.claude]
api = "claude-cli"  # your Claude Code login
model = "claude-sonnet-5-5"

[endpoints.claude-api]
api = "anthropic"
model = "claude-sonnet-5-5"
key_env = "ANTHROPIC_API_KEY"

# [endpoints.openai]
# api = "openai-responses"
# model = "gpt-5.4"
# key_env = "OPENAI_API_KEY"

# [endpoints.local]
# api = "openai-chat"
# base_url = "http://localhost:11434/v1"
# model = "qwen3:32b"

# People you share chats with. Keys other than name, role and ids are their ids per chat app.
# [people.sam]
# name = "Sam"
# role = "member"  # member | guest
# slack = "U0123"

# Group chats Attaché answers in, keyed "<channel>:<chat id>". Unlisted groups are ignored.
# [chats."slack:C0123"]
# name = "Family"
# engage = "mention"  # mention | always | a regular expression matched against the text
# members = ["sam"]   # person ids from [people]
# guests = "ignore"   # ignore | public
# endpoint = "claude-api"

[routing]: which model answers which chat

Every chat is answered by one endpoint, a named [endpoints.<name>] block. [routing] picks the two defaults:

Key Used for Default
own_chats Your terminal and web chat, and your direct messages on chat apps. claude, or whatever you passed to attache init --endpoint
group_chats Every group chat, and direct messages from anyone but you. claude-api

A chat listed under [chats] can name its own endpoint, which wins over both. Your own messages in a group chat also go to group_chats, because other people can read the reply.

So does a page that asks the assistant with attache.ask, even one of your own pages. That turn is the page's, not yours: it sees only the docs shared with the page's chat, and for one of your own pages that means the docs shared with local:me, never everything. See Pages.

The split exists because of Anthropic's terms: a Claude Pro or Max login is for your own use, so the claude-cli backend answers only your own chats and refuses every other turn. Anything that serves other people needs an API key. Keep group_chats on an API endpoint.

Some common setups:

[routing]
own_chats = "claude"        # your Claude Code login
group_chats = "claude-api"  # an API key, per Anthropic's terms
[routing]
own_chats = "claude-api"
group_chats = "claude-api"
[routing]
own_chats = "local"
group_chats = "local"

[endpoints.local]
api = "openai-chat"
base_url = "http://localhost:11434/v1"
model = "qwen3:32b"

Every name you use must have an [endpoints.<name>] block; attache doctor reports one that doesn't.

[endpoints.<name>]: models

Each endpoint picks a wire protocol (api), a model, and where to find it. The block's name is yours to choose; it is what [routing] and [chats] refer to.

Key Meaning
api Required. claude-cli, anthropic, openai-chat or openai-responses.
model The model name, as the provider spells it.
base_url Optional. The server to talk to, for anything other than the provider's own API.
key_env Optional. The name of the environment variable that holds the API key. The key itself never goes in attache.toml.
key_ref Optional. An opaque reference created by attache skill credentials login NAME. The API key is in your OS credential store. Mutually exclusive with key_env.
timeout Optional. Seconds a turn may take before Attaché gives up. Default 900.
max_steps Optional, not claude-cli. The most model calls one of your own turns may make (see below). Default 100. Other people's turns never get more than 20.
context_tokens Optional, not claude-cli. How big a turn's context may grow, in tokens, before it starts afresh from the model's notes (see below). Default 150000. Set it well below the model's context window.
cache Optional, anthropic only. Prompt caching, true or false. Default true for Anthropic's own API, false with a base_url.
max_tokens Optional, anthropic only. The most tokens a single model response may use. Default 4096.
bin Optional, claude-cli only. The Claude Code command, if it isn't claude on your PATH.

If key_env is set but the variable is empty, the first turn on that endpoint fails with a message naming the variable, and attache doctor reports it before that.

Long jobs. On the anthropic, openai-chat and openai-responses routes, Attaché runs the tool loop, and a turn keeps going for as long as it is getting somewhere. It is wound up when it goes round in circles (the same call has given the same result three times; the model is told the second time), reaches max_steps, or has used most (80%) of its timeout. Winding up never just cuts it off: the model is told to stop and gets one last step, with no tools, to say what it did and what is left. The reply ends by saying why it stopped; say "continue" and the next turn picks the work up from there. timeout stays the hard limit, and Stop (or Esc, or Ctrl+C in the terminal) ends a turn at once. For a job longer than one turn should take, raise both max_steps and timeout, or give it to a background job. On claude-cli, Claude Code runs its own loop, and only timeout and Stop bound it.

Context. Within a turn nothing is dropped: every tool call and its result stays in the model's context until the turn ends, so a long turn's context keeps growing. Once one step's context reaches context_tokens, the model is told to write notes on what it has done, found and still has to do, and the turn carries on in a fresh context holding only your request and those notes. It can happen more than once in a turn, and it works the same on every api. Between turns, the model sees the chat's last 30 messages: what you and it said, not the tool calls, which is why a reply that stops early says where it got to. Whatever should outlast that belongs in memory (memory/USER.md, or the chat's notes in memory/chats/).

Each step sends the whole context again. On the anthropic route Attaché marks it for prompt caching (cache), so the part already sent costs a tenth. OpenAI caches long prompts on its own, and local servers such as Ollama, vLLM and llama.cpp reuse a prompt's unchanged start. On claude-cli, Claude Code manages its own context and compacts it when it fills up.

api Use it for Who runs the tool loop
claude-cli Claude Code (claude -p) on your own login. Your own chats only. Claude Code
anthropic The Claude API, or any server that speaks the Anthropic Messages API Attaché
openai-chat Ollama, LM Studio, vLLM, llama.cpp, OpenRouter, Groq, Azure Attaché
openai-responses OpenAI reasoning models with tools Attaché

Store an API key in the OS keyring

Use the keyring for a desktop installation. Keep key_env for deployments that already inject secrets, or services without an accessible user credential store. Existing key_env configurations continue to work and are never migrated automatically.

First define an endpoint. For a custom OpenAI-compatible service, set the API base URL, including its version prefix if needed; do not append /chat/completions or /responses:

[endpoints.custom]
api = "openai-chat"
base_url = "https://models.example.com/v1"
model = "your-model-id"

In your own terminal, run:

attache init
attache skill credentials login custom
attache skill credentials status custom
attache doctor

attache init adds the credentials skill to existing homes without overwriting files. login displays the destination and prompts for the key with hidden input. It writes a credential under service attache-models, verifies it can be read, then adds an opaque key_ref to this endpoint. The key never goes in the TOML file. Comments and other settings are preserved. If this endpoint had key_env, the command removes that setting only after the credential has been saved; it does not change your environment. After checking the new setup, remove the old variable from your shell or service startup configuration yourself.

Set own_chats = "custom" and/or group_chats = "custom" in the existing [routing] block to use it. Restart Attaché after login, rotation, logout or configuration changes: SDK clients keep their resolved key in memory. Neither status nor doctor sends a provider request; they check local availability, so a saved key may still be expired or invalid.

The same commands work for openai-chat (Chat Completions), openai-responses (Responses) and anthropic (Messages). Key storage does not change the request protocol; the configured server must implement the selected API. The legacy OpenAI /completions API is not an Attaché backend. claude-cli continues to use Claude Code's own authentication.

The credential is bound to its normalized API base URL and API family. Changing the host, path, port or family requires running login again. Switching between Chat Completions and Responses on the same base URL, changing a model, or renaming the endpoint preserves access. For key_ref, omitted URLs mean https://api.openai.com/v1/ or https://api.anthropic.com/; ambient OPENAI_BASE_URL and ANTHROPIC_BASE_URL variables do not redirect the saved key. Keyring clients also refuse HTTP redirects; configure the final API URL explicitly in TOML and run login again if it changes. Use HTTPS for remote services.

To rotate a key, run login again. A new record is saved before replacing the reference; the old record is removed after the configuration update, unless another endpoint in the same configuration uses it. References copied to another home on this OS account share the same record: rotating or removing it here can require logging in again in that home. To remove a local key:

attache skill credentials logout custom

logout keeps key_ref so new clients report a missing credential rather than use another source. A manually shared reference is removed for every endpoint using it. Restart Attaché to discard cached keys. Revoke the key with its provider if you want to invalidate it; local removal does not do that. To return to environment credentials, replace key_ref with key_env in the endpoint, set that variable in Attaché's startup environment, and restart.

The supported stores are Windows Credential Manager, macOS Keychain, and Linux Secret Service or KWallet. Attaché refuses plaintext, null and unsupported backends. An unavailable, locked or denied store produces an error; it never silently falls back to an environment variable. Unlock the store or choose key_env explicitly. Services must run as the same OS user with access to that user's store; headless Linux often needs key_env instead.

Copying a home to another machine copies references, not credentials. Run login there. The OS store protects keys at rest; it does not isolate them from other code running as your user. See Security.

claude-cli: Claude Code on your login

[endpoints.claude]
api = "claude-cli"
model = "claude-sonnet-5-5"
# bin = "claude"   # the Claude Code command, if it has another name or isn't on PATH

Install Claude Code and log in once with claude. Attaché then runs claude -p for each turn of your own chats. Claude Code runs its own tool loop with its own Read, Write, Edit, Glob, Grep, Bash, WebFetch and WebSearch tools, and reaches Attaché's tools (to-dos, reminders, pages, skill scripts) through an MCP endpoint on the local web server. model is passed to claude --model.

  • It keeps one Claude Code session per chat and resumes it, so a long chat doesn't resend its history. A fresh session gets the recent conversation (the last 30 messages, up to 20,000 characters) as a transcript. /new starts a fresh session with no transcript.
  • It runs in a folder next to your home, ~/.attache-claude, so a CLAUDE.md or .claude/settings.json inside ~/.attache never loads as Claude Code project settings. Your own ~/.claude settings still apply.
  • It needs no key_env. If ANTHROPIC_API_KEY is set in the environment Attaché starts in, Claude Code uses that key instead of your login.

What this backend may do is covered in Security.

anthropic: the Claude API

[endpoints.claude-api]
api = "anthropic"
model = "claude-sonnet-5-5"
key_env = "ANTHROPIC_API_KEY"
# max_tokens = 4096

Create a key in the Claude Console and put it in the environment before you start Attaché:

export ANTHROPIC_API_KEY="sk-ant-..."
attache run
$env:ANTHROPIC_API_KEY = "sk-ant-..."
attache run

Set base_url to use any other server that speaks the Anthropic Messages API.

openai-chat: Ollama and other compatible servers

Chat Completions is the format most self-hosted and third-party servers share. For a model running in Ollama on this machine:

ollama pull qwen3:32b
[endpoints.local]
api = "openai-chat"
base_url = "http://localhost:11434/v1"
model = "qwen3:32b"
context_tokens = 24000  # well below the context length the server gives the model

An unauthenticated local server needs no key, so leave both key_env and key_ref out. Pick a model that supports tool calling: Attaché's to-dos, reminders and pages all work through tools. A local model's context is often far smaller than a hosted one's, and a server may cut a longer prompt short without saying so, so set context_tokens below it (in Ollama, the context length is a setting of the server or the model).

For a hosted service, set its base_url and the variable that holds its key:

[endpoints.openrouter]
api = "openai-chat"
base_url = "https://openrouter.ai/api/v1"
model = "..."  # the model id, as OpenRouter lists it
key_env = "OPENROUTER_API_KEY"

openai-responses: OpenAI reasoning models

OpenAI's reasoning models need the Responses API for tool calls, so they have their own adapter:

[endpoints.openai]
api = "openai-responses"
model = "gpt-5.4"
key_env = "OPENAI_API_KEY"

Attaché sends each request with store off, so OpenAI keeps no conversation state: the reasoning comes back encrypted, and Attaché replays it from its own copy of the turn.

[web]: the local web page

[web]
host = "127.0.0.1"
port = 8765
Key Meaning
host 127.0.0.1 (the default), localhost or ::1. Attaché serves this machine only, and refuses to start with any other address.
port The port for the web page. Default 8765. Change it if something else uses that port.
public_url Optional. The https:// address where people off this machine open share links, such as https://pc.tailnet.ts.net: a host and perhaps a port, no path. Empty or left out, sharing is off.
public_port The port on 127.0.0.1 that serves share links when public_url is set. Default port + 1, so 8766 (65534 when port is 65535). It must differ from port.

If a port is taken when you start, Attaché stops with a message saying so:

$ attache run
Port 8765 is in use. Is `attache run` already running?

A page a member made, or one made in a group chat, can be shared with a link that works off this machine, for 7 days and while the page stays as it is. Attaché still listens on this machine only: Tailscale Funnel carries the requests from the internet to it.

  1. Install Tailscale, log in, and allow Funnel for this machine in your tailnet's settings.
  2. Set public_url to the machine's Funnel address:

    [web]
    port = 8765
    public_url = "https://pc.tailnet.ts.net"
    # public_port = 8766   # the default: port + 1
    
  3. Restart Attaché, then point Funnel at public_port, which serves share links only:

    $ tailscale funnel --bg 8766
    

Point Funnel at public_port, never at port. The server on public_port has only the share link routes and what a shared page needs to run (its data and live updates); your chat, your own pages and Claude Code's MCP endpoint are not there at all, and it answers only requests for the public_url host. tailscale funnel reset turns it off. What a share link can reach is in Pages and Security.

[assistant] and [owner]

[assistant]
name = "Attaché"

[owner]
name = "Sam"

[owner.ids]
slack = "U0123ABCD"
  • [assistant] name is how the assistant names itself, and the word that addresses it in a group chat.
  • [owner] name is your name. Everything typed in the terminal or on the web page comes from you.
  • [owner.ids] holds your own sender id on each chat app, so your direct messages there reach you as the owner. An id can also go straight in [owner], as slack = "U0123ABCD". Empty ids are ignored.

[people]

Phase 2

Chat apps (Slack and Signal) arrive in phase 2. Until then the only chat is your own, in the terminal and on the web page, and [people] and [chats] have nothing to act on. Attaché already reads them, so you can see how group chats will work.

People are keyed by an id you choose, and found by their sender id on each chat app, never by display name:

[people.jo]
name = "Jo"
role = "member"   # member | guest
slack = "U04JOJOJO"
Key Meaning
name How Attaché labels them in the conversation. Defaults to the id.
role member (the default) or guest, for direct chats with them. In a group, their role comes from that chat's members.
ids A table of channel ids, as an alternative to plain keys.
any other key Their sender id on that channel, such as slack = "U04JOJOJO".

Someone who isn't listed can't start a direct chat with Attaché: their messages are ignored.

[chats]: group chats

Attaché answers only in the group chats listed here, keyed "<channel>:<chat id>". It ignores every other group.

[chats."slack:C0123"]
name = "Family"
engage = "mention"
members = ["jo"]
guests = "ignore"
endpoint = "claude-api"
Key Meaning Default
name A name for the chat, used in the prompt. the chat key
engage When Attaché answers: mention, always, or a regular expression searched in each message. mention
members Person ids from [people] who are members in this chat. none
guests ignore or public: whether Attaché answers people who aren't members. ignore
endpoint The endpoint for this chat, instead of [routing] group_chats. none

With engage = "mention", Attaché answers a native mention or a reply to one of its messages, or a message containing its name as a word, in any case and with or without the accent (attache matches Attaché). Messages it doesn't answer are still kept as context for later turns.

In a listed group, you are the owner, the people in members are members, and everyone else is a guest. With guests = "ignore", guests' messages are neither answered nor stored. With public, Attaché answers them too, without any tools. What each role may do is in Security.