Exec Mode¶
ha-repl exec runs one snippet of Python against a running Home Assistant and exits. It is the non-interactive form of live mode, and its main use is giving a coding agent a way to check its ideas against a real instance: read an entity's state, query the recorder, call a service, inspect an object, then decide what to do next.
It needs the same server component as live mode - read the warnings on the live mode page first: an agent with exec can run any Python inside Home Assistant. Point it at a devcontainer or development instance, not the one running your house.
Setup¶
The agent needs the ha-repl command and two environment variables:
Both can also go in a .env file in the directory the agent runs from, or the server can be one named in a config.toml, where a repo's .ha-repl/config.toml can set the default for everything run in that repo. Plugins are run before the snippet, so helpers defined there are available to the agent; --no-plugins skips them. Then tell the agent the command exists, for example in its project instructions:
Use `ha-repl --json exec '<python>'` to run Python against the development
Home Assistant. `hass` is the live HomeAssistant object, `obj` the object tree,
`sql("select ...")` queries the recorder. See [Exec Mode](https://homeassistant-repl.rhizomatics.org.uk/exec_mode/) documentation.
Agent Skill¶
The repo also ships an Agent Skill, skills/ha-repl, which teaches an agent the command, its JSON output and the rules on this page, so you don't have to write the instructions yourself. Install it in the project where you develop your component.
For Claude Code, as a plugin:
claude plugin marketplace add rhizomatics/agent-plugins
claude plugin install homeassistant-repl@rhizomatics
For other agents that support skills, copy the skills/ha-repl directory into the agent's skills directory (for example .claude/skills/ or .agents/skills/), or use the skills installer:
Documentation for Agents¶
Every page of this site is also published as Markdown: add index.md to a page's address, or use the Markdown button at the top of the page. llms.txt lists the pages with a line on each, and llms-full.txt is all of them in one file.
Running a Snippet¶
Pass the code as an argument, from a file, or on standard input. Standard input avoids shell quoting problems and is the best form for anything longer than a line.
ha-repl exec 'hass.states.get("sun.sun").state'
ha-repl exec -f snippet.py
ha-repl exec - <<'PY'
r = sql("select entity_id from states_meta", max_rows=3)
{row[0]: hass.states.get(row[0]).state for row in r}
PY
The value of the last expression is printed, as at a Python prompt. await works at the top level, and a call to an async function is awaited for you if you leave the await out.
| Option | Effect |
|---|---|
--json |
Print one JSON object instead of text. Goes before exec |
-s NAME, --session NAME |
Use a named session inside Home Assistant (default default). Goes before exec |
-t SECONDS, --timeout SECONDS |
Give up after this long |
--reset |
Clear the session inside Home Assistant before running |
-f FILE |
Read the snippet from a file |
The exit status is 0 on success, 1 if the snippet raised or timed out, and 2 if Home Assistant could not be reached or the arguments were wrong.
JSON Output¶
With --json the result is one object on standard output, whether or not the snippet succeeded:
{
"stdout": "",
"value": {
"columns": ["metadata_id", "entity_id"],
"rows": [[1, "zone.home"], [2, "conversation.home_assistant"]],
"rowcount": 2,
"truncated": true
},
"error": null,
"duration": 0.004,
"truncated": false
}
| Field | Content |
|---|---|
stdout |
Everything the snippet printed |
value |
The last expression, or null if the snippet ended in a statement |
error |
null, or an object with type, message and traceback |
duration |
Seconds the snippet took |
truncated |
true if output from inside Home Assistant was cut at its size limit |
What value holds depends on where the last expression ran:
- A
sqlresult is its data:columns,rows(one list per row),rowcount, and its owntruncated, which istruewhen the query hitmax_rowsand there were more rows to fetch. - Any other local value that is already JSON-shaped (numbers, strings, lists, dicts) is passed through as it is. Anything else is its
repr(). - A value from inside Home Assistant follows the same rule when it is plain data, so
hass.states.get("sun.sun").stategives"below_horizon". Anything else - a state object, or a list containing one - is a string holding itsrepr(), so shape the answer into plain data inside the snippet.
What Runs Where¶
A snippet is ordinary Python running in the ha-repl process, with whatever is installed there. Statements that use hass or obj are the exception: they are sent to Home Assistant and run inside it. The rules are the same as the interactive shell and are described in full under What Runs Where. The points that matter most for a script:
sqlandhass_apiare local. A query's result is downloaded, and everything done with it afterwards happens locally.- Plain local data (strings, numbers, lists and dicts of them) and
sqlresults can be used in a statement that runs inside Home Assistant. Asqlresult arrives there as a list of rows. - A variable assigned by a statement that used
hassorobjcomes back, and is local afterwards, if it holds plain data. Anything else stays inside Home Assistant, and later statements that use it run there too. - Don't use
sqlorhass_apiin the same statement ashassorobj- it is refused. Assign one part on its own line first.
Mixing Both Sides has the details and worked examples.
State Between Calls¶
Each exec call starts a new local Python process, so local variables and imports do not carry over. Variables inside Home Assistant do: they live in the named session until it is reset or Home Assistant restarts.
ha-repl exec 'entry = hass.config_entries.async_entries("sun")[0]'
ha-repl exec 'entry.state' # still there
ha-repl sessions # list sessions and their variables
ha-repl reset # clear the default session
Give each agent, or each task, its own --session name if several run against the same instance, so they don't overwrite each other's variables.
Errors¶
Without --json, a traceback goes to standard error and the exit status is 1. With --json, error is filled in and stdout still holds whatever was printed before the failure:
{
"stdout": "",
"value": null,
"error": {
"type": "ZeroDivisionError",
"message": "division by zero",
"traceback": "Traceback (most recent call last):\n File \"/ha_repl/api_cell_1\", line 1, in <module>\n x = 1/0\n ~^~\nZeroDivisionError: division by zero\n"
},
"duration": 0.001,
"truncated": false
}
A snippet stops at the first statement that fails. Statements before it have already run, including any that changed something inside Home Assistant.
For an error raised inside Home Assistant, type and message are plain, but traceback is formatted for a terminal with box-drawing characters. Rely on type and message when parsing.
A rejected query raises SqlError. Only a single SELECT statement is accepted.
Tips for Agents¶
- Use
--json, and send the snippet on standard input. - Always set
-t. A snippet that waits on something that never happens otherwise blocks forever. - Keep
max_rowssmall while exploring. The default is 1000 rows;sql.tableslists the tables and their columns without running a query. - Prefer reading to writing.
hass.states.get(...),obj[...]andsql(...)are safe to repeat; service calls and direct changes tohassobjects take effect immediately on the instance. help(thing)prints a summary of an object's methods and properties, which is often quicker than reading the source.help(thing, full=True)gives Python's own full help page instead.