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:
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:
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.
/newstarts a fresh session with no transcript. - It runs in a folder next to your home,
~/.attache-claude, so aCLAUDE.mdor.claude/settings.jsoninside~/.attachenever loads as Claude Code project settings. Your own~/.claudesettings still apply. - It needs no
key_env. IfANTHROPIC_API_KEYis 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é:
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:
[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:
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¶
| 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:
Share links through Tailscale Funnel¶
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.
- Install Tailscale, log in, and allow Funnel for this machine in your tailnet's settings.
-
Set
public_urlto the machine's Funnel address: -
Restart Attaché, then point Funnel at
public_port, which serves share links only:
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] nameis how the assistant names itself, and the word that addresses it in a group chat.[owner] nameis 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], asslack = "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:
| 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.