RFC 0001: Persistent Configuration and Startup Files¶
| Status | Draft, for discussion |
| Date | 2026-10-07 |
| Affects | ha-repl command, homeassistant_repl.connect(). No change to the server component |
Summary¶
Add a configuration directory, ~/.config/ha-repl/, holding two things:
config.toml, which names the Home Assistant servers you work with, so one can be picked by namestartup/, a directory of plain Python files run at the start of every session, for bookmarked expressions, helper functions and classes
A repo can carry the same layout in a .ha-repl/ directory, which is layered over the one in the home directory.
Both mechanisms are borrowed: named servers from kubectl contexts and AWS profiles, and the startup directory from IPython. Everything that works today (--server, --token, HASS_SERVER, HASS_TOKEN, .env) keeps working and keeps its precedence.
Motivation¶
Three things are awkward at present:
- More than one Home Assistant. A component developer usually has a devcontainer instance, perhaps a test instance, and the one running the house. Switching means re-exporting two environment variables or swapping
.envfiles, and it is easy to end up with the server from one and the token from another. - Nothing persists. A useful expression such as
hass.data["entity_components"]["sensor"], or a five line helper, has to be found in history or retyped in every session. Variables inside Home Assistant last until it restarts; local ones don't outlive the process. - Work spans repos. A developer with several components wants the same servers and helpers in each, plus a few things specific to one component. A
.envin each repo doesn't share anything.
The configuration should also be easy to keep in a dotfiles manager such as chezmoi, which means plain text files in a predictable place, with a way to keep tokens out of them.
Design¶
Where configuration lives¶
~/.config/ha-repl/ # $XDG_CONFIG_HOME/ha-repl if that is set
config.toml
startup/
10-helpers.py
20-bookmarks.py
<repo>/.ha-repl/ # nearest one walking up from the working directory
config.toml
startup/
50-this-component.py
~/.config is used on every platform, including macOS, as git, gh and uv do. It is the location dotfiles tools expect.
The repo directory is found by walking up from the working directory and stopping at the first .ha-repl/, or at the root of the git repository if none is found.
Both directories are optional, and so is each file within them.
config.toml¶
default = "dev" # server used when none is named
session = "default" # same as --session
ttl = 30 # same as --ttl
auto_await = true # false is the same as --no-auto-await
[servers.dev]
url = "http://localhost:8123"
token = "eyJ..."
[servers.house]
url = "https://ha.example.org"
token_command = "op read op://Private/ha-house/token"
[servers.test]
url = "http://ha-test.local:8123"
token_env = "HA_TEST_TOKEN"
A server needs a url and one of:
| Key | Token comes from |
|---|---|
token |
The file itself |
token_command |
The standard output of a command, run without a shell |
token_env |
A named environment variable |
token_command is what lets the file sit in a public dotfiles repo. A plain token suits a chezmoi template that fills it in from a secrets manager.
The repo file is merged over the home file: top-level keys are replaced, and servers is merged by name and then key by key. A typical repo file is one line:
Choosing a server¶
--server and HASS_SERVER accept either a configured name or a URL. A value with no :// in it is looked up as a name, and it is an error if no server has that name.
ha-repl --server house live
HASS_SERVER=test ha-repl exec 'hass.config.version'
ha-repl live # uses `default`
ha-repl --server http://10.0.0.5:8123 --token ... live # unchanged
The interactive banner shows the name as well as the address, and ha-repl servers lists what is configured and which is the default, without printing tokens.
Precedence¶
For both the server and the token, the first of these that gives a value wins:
--server/--tokenHASS_SERVER/HASS_TOKENin the environmentHASS_SERVER/HASS_TOKENin a.envfile in the working directorydefaultin the repoconfig.tomldefaultin the homeconfig.tomlhttp://homeassistant.local:8123, with no token
When the server is given by name, the token comes from that server's entry unless --token is given. HASS_TOKEN is not mixed with a named server: pairing a token from the environment with a server from the config is the mistake this feature is meant to remove. If the named server has no usable token, that is an error.
Inside a Home Assistant add-on, SUPERVISOR_TOKEN keeps its present behaviour when no server is given.
Startup files¶
Every *.py file in startup/ is run at the start of a session, home directory first and then the repo's, each in name order. A later file can redefine a name from an earlier one, so a repo can override a helper.
# ~/.config/ha-repl/startup/20-bookmarks.py
import datetime as dt
sensors = hass.data["entity_components"]["sensor"]
def stale(hours=24):
"""Entities not updated in the last `hours`."""
cutoff = dt.datetime.now(dt.UTC) - dt.timedelta(hours=hours)
return [s.entity_id for s in hass.states.async_all() if s.last_updated < cutoff]
A file is run as though its contents had been typed at the prompt:
- In live mode the usual What Runs Where rules apply statement by statement.
staleabove useshass, so it is defined inside Home Assistant; theimportis local, and is repeated there becausestaleneeds it. - In
apimode there is nohass. Definingstaleworks, since the name is only looked up when it is called, but thesensorsline fails. - A statement that fails is reported as a warning on standard error, naming the file and line, and the rest of the file and the remaining files still run. A session always starts.
- Nothing is echoed. The value of a bare expression in a startup file is discarded.
The interactive banner lists the files that were loaded. --no-startup skips them all.
Startup files are also run by ha-repl exec, so a snippet and an agent see the same names as the interactive shell. With --json, a startup warning goes to standard error and does not appear in the JSON object.
Use from other Python contexts¶
homeassistant_repl.connect() reads the same config.toml:
Startup files are ordinary Python with no special syntax, so the same file can be named in PYTHONSTARTUP, linked into IPython's own startup directory, or run with exec(open(path).read()) in a notebook. Only the lines that use hass depend on live mode.
Security¶
A repo's .ha-repl/ directory is code and configuration that arrives with a clone. Left unchecked it could:
- run arbitrary Python locally, through a startup file
- run arbitrary Python inside whichever Home Assistant is the default, through a startup file that uses
hass - run an arbitrary command, through
token_command - point
defaultat an address of its own choosing
The last of these is the reason the token is never taken from the environment for a server named in a config file.
The proposal is that a repo directory is ignored until it has been approved, as direnv does with .envrc:
$ ha-repl live
ha-repl: ignoring /work/mycomponent/.ha-repl - run `ha-repl trust` to allow it
$ ha-repl trust
Approval is recorded in ~/.local/state/ha-repl/ against the directory's path and a hash of its contents, so a change to any file in it needs approving again. The home directory is always trusted.
Tokens are never printed, by ha-repl servers or in error messages. A config.toml that holds a token and is readable by other users gets a warning.
Alternatives Considered¶
A separate --profile flag. --server would stay as a URL only, and --profile NAME would choose an entry. It is more explicit, but gives two flags that both say where to connect and a rule for what happens when both are given.
More .env files. .env.house, .env.dev and a flag to pick one. This is close to what exists, but it gives no home directory sharing, no place for startup code, and no way to keep a token out of the file.
[tool.ha-repl] in pyproject.toml. Familiar from ruff and uv, and no new directory in the repo. It can't hold startup files, so a second location would be needed anyway, and not every component repo has a pyproject.toml.
A single startup file, as PYTHONSTARTUP has. Simpler, but a directory lets the home and repo sets be combined without one including the other, and lets chezmoi manage each file separately.
Bookmarks as data, for example a [bookmarks] table of name to expression. Easier to list and to validate, but it can't hold a function or a class, and it would be a format nothing else understands.
Storing bookmarks inside Home Assistant, in the server component. They would follow the instance, not the developer, and it adds state to the side of the design that is meant to stay small (see Design Principles).
Open Questions¶
--server NAME|URL, or a separate--profile? This document proposes the first.- Should
execrun startup files by default? This document proposes yes, with--no-startup. It adds a round trip to Home Assistant for each file that useshass, on every call. .ha-repl/directory, or a single.ha-repl.toml? This document proposes the directory, to match the home layout.- Is the trust step worth its cost? It is the largest piece of new code here. The alternatives are to document the risk, or to allow a repo
config.tomlbut not repo startup files ortoken_command. - Should a server entry be able to refuse writes? For example
live = falseon the house instance, so thatliveandexecare refused against it by name. It would be a guard against a slip, not a security control. - Should startup files be able to tell which server and mode they are running against? For example
SERVERandMODEvariables, so one file can hold lines for live mode only. Left out for now.
Out of Scope¶
- Saving a variable or function from a running session into a startup file
- Startup files that apply to one named server only
- Any change to the server component or its protocol