Chat with Claude from Home Assistant Assist, and call it from your automations — powered by the companion Claude Code app (a Home Assistant app, formerly called an add-on) running on your own hardware. Requires Home Assistant OS or Supervised. Home Assistant lists the integration as AI Agent.
claude_ha exposes Claude to Home Assistant itself. It talks to Claude through
the Claude Code app over Home
Assistant's internal network — the app holds the login and runs Claude, while
this integration is a thin, secure client. Nothing here calls the Anthropic
cloud directly.
| 💬 Conversation agent | Talk to Claude from HA Assist — replies stream in live, it acts on benign requests immediately and confirms important ones (judged per action). |
| 🧠 Memory between conversations | Ask it to remember a fact ("my room is the office") and it keeps it for later conversations — saved only after your yes. |
| 🌐 Web answers | Optionally, a separate "(web)" Assist agent answers from the web alone — no view of your home, no actions. Off by default. |
| 👁️ Camera vision | Optionally, a visual question ("who's at the door?") sends one snapshot of an Assist-exposed camera to Claude. Off by default. |
| 🗨️ Dashboard chat card | A bundled Lovelace card to chat with Claude and Apply/Dismiss its suggestions. |
⚙️ claude_ha.ask action |
Send a prompt to Claude from automations, optionally confirming changes via a phone notification. |
| 📟 Sensors | App readiness, active model, and Claude token usage + prompt-API cost. |
| 🩺 Health checks | Detects the "chat can't see your home" gaps and raises a repair with the exact fix. |
| 🎙️ Local voice one-click | claude_ha.setup_voice installs Whisper + Piper and builds an Assist pipeline for your language. |
| 🔒 Secure by design | Read-only by default, bearer-token auth, scoped writes, no cloud calls. |
| 🚀 Zero-touch setup | Discovered, installed and started for you via the Supervisor. |
- How it works
- Requirements
- Installation
- Configuration
- Usage
- Security model
- How data is updated
- Known limitations
- Troubleshooting
- Removal
- Development
- License
HA Assist ─┐
├─► claude_ha (this integration) ──HTTP + bearer──► Claude Code app ──► Claude
Automation ┘ conversation entity (own agentic loop,
claude_ha.ask action scoped HA access)
The integration and the app are separate projects that connect through a small, versioned HTTP contract (a bearer-authenticated prompt server on the app's internal port). The connection details — host, port and a shared token — are handed to the integration automatically through Supervisor discovery, so there is nothing to type in.
-
Home Assistant OS or Supervised (the integration manages a Supervisor app).
-
One companion app for the AI agent you want to talk to — Home Assistant doesn't ship either. Add the repository URL of the one you want once, then install the app from the App store:
- Claude Code —
https://github.com/LayerTM/ClaudeInHA - Codex —
https://github.com/LayerTM/CodexInHA
To add a repository by hand: Settings → Apps → Install app, then ⋮ (top right) → Repositories, paste the URL and select Add. Open the app in the App store, select Install, then Start. To keep it one click away, turn on Show in sidebar on the app's Info tab; Open Web UI is there too.
The integration's setup flow can also add the repository for you once you pick which agent to set up (Settings → Devices & services → Add integration).
- Claude Code —
This integration is listed in the HACS default store. A freshly listed repository takes a few hours to reach every HACS install, so the button below is the reliable route in the meantime — if your HACS doesn't know the repository yet, it offers to add it.
- Click Open in HACS above and install AI Agent. (Accept the prompt if
HACS asks to add the repository first.) You can also search HACS for
claude-ha, which matches only this integration. - Restart Home Assistant.
- Install the Claude Code or Codex app. Once it starts, Home Assistant offers to set up the AI Agent integration automatically — accept it. No configuration needed.
Neither route working? Add LayerTM/claude-ha as a custom repository
(category Integration). If HACS replies that the repository already exists in
the store, then it is already listed — search for claude-ha instead.
Copy custom_components/claude_ha into your Home Assistant
config/custom_components/ directory and restart.
Setup is zero-touch: when a companion app starts it advertises its host, port and a freshly generated token through Supervisor discovery, and Home Assistant surfaces a one-click setup. You can also add it from Settings → Devices & services → Add integration → AI Agent, which first asks which agent to set up, then finds, installs (offering to add its repository if needed) and starts the app for you. There are no options to fill in. If more than one app of the chosen agent is installed (for example a store build and a local build), setup asks which one to use; each one can be set up as its own entry, one agent per entry.
Each entry remembers which AI engine its app runs and checks it on every status poll. If the app ever reports a different engine, the entry goes unavailable and a repair explains why; nothing is sent to it until the engines match again.
Select Claude (conversation.claude_code) as a conversation agent under
Settings → Voice assistants, or target it directly. It answers in any language, and it can act on your home.
With Prefer handling commands locally on, Home Assistant runs the device
commands it recognises exactly (such as "turn off the kitchen light") itself,
without the confirmation described below, and passes everything else — state
questions included — on to Claude. Leave it off to have every request go
through Claude.
Answers stream in live, token by token (with the Claude Code app ≥ 1.17.0;
older apps return the whole answer at once).
Every request is read first. If it would change state, Claude proposes the exact actions, and the integration decides — per action, from live entity metadata — whether to carry it out immediately or confirm:
- Benign, low-risk actions run right away (e.g. turning a light on) and reply
Done: …. If Home Assistant refuses an action, the reply names the targets that failed instead (with an app that reports each action's outcome). - Important actions are held and confirmed with a plain
yes/no— anything the deterministic classifier flags (locks, alarms, garage/door/gate covers, firmware updates, router/AP and other config entities, and opaque-effect wrappers like scenes, scripts and automations), anything Claude itself marks non-low-risk, or anything you pin as critical. The classifier only auto-runs an action it can positively prove benign — anything it can't resolve or bound falls back to confirmation. The confirmation replays the exact validated actions, so it never depends on the model remembering them.
Criticality is judged per action, not per domain — a shade and a garage door are
both cover, but only the garage door is confirmed. Requires the Claude Code
app ≥ 1.8.0 (older apps simply confirm everything). Tune it under the
integration's Configure options: turn off auto-execute to confirm every
change, or list entities that must always be confirmed.
A change to an entity listed under "Always confirm these entities": Assist
shows what it would do and which entities it affects, and waits for your
yes or no. Nothing changes until you answer yes.
Claude can only ever touch entities you have exposed to Assist — that exposure list is the outer ceiling; the per-action classifier is the inner gate.
Camera vision (opt-in, off by default; app ≥ 1.17.0). Turn on Let Claude look at cameras in the options and a clearly visual question — "who's at the door?", "look at the backyard camera" — sends one snapshot to Claude. Only a camera you have exposed to Assist is ever sent, only when the message is visual and exactly one camera resolves (by name, area or floor); it never guesses between cameras. The app fetches and downscales the snapshot itself; the integration passes only the entity id.
Web answers (opt-in, off by default; needs an app release that supports them, switched on in the app's options). Turn on Answer from the web (separate agent) in the options and a second Assist agent, Claude (web), appears next to the first. It answers a question from the web alone: it never sees your home, remembers nothing, is not part of the conversation history and cannot act on anything — whatever the app sends back, only its text is shown. Your home agent never uses the web. If the app has web answers switched off, the agent says so and Home Assistant shows a repair until the next web answer succeeds.
Memory between conversations (on by default). Ask Claude to remember a
short fact — "remember that my room is the office", "sleep mode means the
office and hall lights off" — and it shows the fact and asks you to confirm;
it is kept only after your yes. Later conversations take it into account
("turn off my room" then means the office). "Forget where my room is" removes
it the same way, after a yes.
- What is kept: only facts you asked it to remember, one line each, at most 200 characters, at most 50 per person and 50 for the household. Never device states, answers, or anything Claude worked out on its own.
- Whose facts: facts you ask for in your own Home Assistant account are yours; another person never sees them. Household facts are shared by everyone, including turns without an account (voice satellites, automations). Because they reach everyone's conversations, only an administrator can add or remove them — in chat, or with the actions below (an automation calling the actions counts as set up by an administrator). A turn without an account uses the household's facts but never changes them: a spoken "yes" does not tell who is speaking.
- What they can do: they are background for Claude, not instructions — they never unlock an action or skip a confirmation.
- See, edit and clear them: the actions
claude_ha.list_memory,claude_ha.rememberandclaude_ha.forget(one fact by id, or all you may remove). Diagnostics show only how many facts are stored. Removing the integration deletes them; turning off Remember facts between conversations in the options stops sending and offering them. A fact added withclaude_ha.rememberalso appears in Home Assistant's event history, like the data of any action call;claude_ha.forgetdoes not remove it there. - One question at a time: if a request both asks for an action and to remember something, the action is confirmed and the fact is not kept — the reply says so; ask to remember it on its own.
Chat uses the facts with a Claude Code or Codex app release that supports
them; with an older one they are still kept and listed, and
claude_ha.list_memory reports used_in_chat: false.
A chat card ships with the integration — no separate install: the integration
adds it to your dashboard resources (Settings → Dashboards → ⋮ → Resources)
and keeps that entry current. If your resources are managed in YAML
(lovelace: resource_mode: yaml), add it there yourself:
lovelace:
resources:
- url: /claude_ha/claude-chat-card.js
type: moduleAdd a Manual card (or pick Claude Chat in the card picker) with:
type: custom:claude-chat-card
title: ClaudeType a message and the card shows Claude's reply. If Claude proposes a change, an inline Apply / Dismiss appears; Apply runs the confirmed write.
With more than one Claude entry, pick the one the card talks to in the card
editor, or set its entry id in YAML (config_entry: <entry id>). Without it the
card works only while a single entry exists.
action: claude_ha.ask
data:
prompt: Summarise today's calendar and suggest what to wear.
response_variable: claudeclaude.text holds Claude's answer; the response also includes proposal (a
described state change, or null), tools_used, and truncated.
Add web: true for an answer from the web alone: text only, no view of your
home, no memory, and proposal is always null. It needs an app that
supports web answers with the option switched on, and cannot be combined with
mode: write, intents or notify.
To act on something, use the two-phase flow: a read call returns a proposal
with intents; after your own confirmation, echo those exact intents back in a
write call. Writes are scoped by the Claude Code app to just those confirmed
intents — only use write from automations you control, never on untrusted input.
# 1. Ask (read) — get the proposed intents.
- action: claude_ha.ask
data:
prompt: Turn off everything in the garage.
response_variable: claude
# 2. Confirm, then act (write) — echo the confirmed intents back.
- action: claude_ha.ask
data:
prompt: Turn off everything in the garage.
mode: write
intents: "{{ claude.proposal.intents }}"Or let Home Assistant ask you to confirm on your phone: pass a notify target,
and if Claude proposes a change you get an actionable Approve / Dismiss
notification — Approve runs the confirmed write for you.
action: claude_ha.ask
data:
prompt: Turn off everything in the garage.
notify: mobile_app_my_phoneThe full set the integration creates, all on one device. The first two are meant to be acted on; the rest are diagnostic, which Home Assistant keeps out of the device's main controls by default.
- Claude (
conversation.claude_code) — the conversation agent itself: pick it under Settings → Voice assistants, or target it from a script or automation. See Conversation agent. - Active alerts (
binary_sensor.claude_code_active_alerts) — aproblembinary sensor, on while the app has any active proactive alert (leak, offline, battery, CO₂ and the rest). It mirrors the app's own alert set, so your automations and your history can react to it instead of only the push notification.active_count,critical_countand anitemslist (each with itskey, acriticalflag and the one-line reading) are the attributes;criticalmarks the alerts the app always sends. The lines are your own entity names and readings — home data you already have in Home Assistant, never chat content. Needs app ≥ 1.39.0, and stays unavailable while proactive alerts are off or have not run yet. - Status (
sensor.claude_code_status) —ready/initializing, with the app version, Claude version, the engine and its version, active model, whether a scoped HA MCP is configured and reachable, ahealthsummary and the count of entities exposed to Assist as attributes. - Chat health (
sensor.claude_code_chat_health) — a rolling summary of recent chat outcomes (ok/degraded). It asks whether failures are still happening, not whether any ever did. Three failed chats in a row raises it straight away — that's an outage, whatever the longer-run numbers say — and so does one chat in ten or more failing. It clears again once the clean chats since the last failure outnumber the failures behind them, or that failure is over 6 hours old. So a single old blip no longer nags for days, while a problem that is happening right now shows up before it has had time to move an average, and one chat getting through mid-outage isn't enough to call it recovered. The attributes show the working: recent/ok/degraded/recovered counts, thefailure_ratethe state turns on,consecutive_okandconsecutive_failed(chats since the last failure, and since the last success), the last degrade reason (a token, never prompt content) and the window's timestamps. Needs app ≥ 1.20.0; unavailable otherwise. The run counters and the timestamps need app ≥ 1.49.0 and are null without it — an app that reports none of them is judged on the rate alone, so a missing field neither quiets a warning nor invents one.window_dated(app ≥ 1.55.0, null without it) is how many of therecentchats carried a stamp, and it is what makes the two timestamps readable: equal torecentthey span the whole window, and at 1 they are a single instant printed twice rather than a range. - Daily budget spend (
sensor.claude_code_daily_budget_spend) — today's spend in USD against the app's daily budget, withlimit/remaining/fraction_used/ a softnear_capflag as attributes (a limit of 0 = unlimited leaves those null). Diagnostic; never a repair. Needs app ≥ 1.21.0. If the app reports a spend or a cap that cannot be read as a plain amount, the sensor goes unavailable rather than showing a figure nobody can stand behind — an unreadable cap is never treated as no cap. - Token usage today (
sensor.claude_code_token_usage_today) — today's input + output tokens (unittokens), with the full usage report (per-period and per-model token totals, message counts) as attributes. Polled slowly (~5 min; the app caches it). If the app's usage history was lost and could not be recovered, the report carrieshistory_resetandhistory_since, and a one-time repair notice under Settings → Repairs names the day totals now count from. - Prompt API cost (
sensor.claude_code_prompt_api_cost) — the total prompt-API cost in USD (interactive-console use is measured in tokens, not dollars). The usage/cost sensors need the Claude Code app ≥ 1.7.0 and stay unavailable otherwise. - Account limits — how much of your Claude account's rate limits is used,
across every machine and session signed in to it, not just this app:
Session limit (
sensor.claude_code_session_limit), Weekly limit (sensor.claude_code_weekly_limit) and one Weekly limit (model) sensor per model the account limits separately (e.g.sensor.claude_code_weekly_limit_fable). Each reads 0-100 %, withresets_at,severityandkindas attributes. The sensors are built from what the account reports, so a new limit appears on its own and one that stops being reported goes unavailable rather than disappearing. Accounts signed in with an API key have no such limits and get no sensors at all. Needs an app that provides account limits; older ones simply don't create these sensors. - Check Claude health (
button.claude_code_check_claude_health) — runs the deeper probe described under Health checks on demand.
Entity ids above are the defaults (device Claude Code + entity name); adjust to your own if you've renamed them.
A ready-made overview using only built-in cards — paste it into a dashboard (Edit
dashboard → Add card → Manual) to see cost, budget, chat health and MCP state at a
glance. No extra install; adjust the entity ids if yours differ, and set the gauge
max to your app's daily budget cap.
type: vertical-stack
cards:
- type: entities
title: Claude
entities:
- entity: sensor.claude_code_status
name: Status
- entity: sensor.claude_code_chat_health
name: Chat health
- entity: sensor.claude_code_daily_budget_spend
name: Budget spent today
- entity: sensor.claude_code_token_usage_today
name: Tokens today
- entity: sensor.claude_code_prompt_api_cost
name: Total API cost
- type: gauge
entity: sensor.claude_code_daily_budget_spend
name: Daily budget
needle: true
max: 5 # set to your app's daily budget cap
severity:
green: 0
yellow: 3.5
red: 4.5
- type: history-graph
title: API cost (24 h)
hours_to_show: 24
entities:
- sensor.claude_code_prompt_api_cost
- type: history-graph
title: Tokens today (24 h)
hours_to_show: 24
entities:
- sensor.claude_code_token_usage_todayCost (USD) and tokens are on separate graphs on purpose — a single graph shares one y-axis, so the small USD range would be flattened against the much larger token count. The budget gauge is only meaningful with a daily budget set on the app; with no budget (unlimited) the spend still shows but the gauge scale is arbitrary.
The most common "dead on arrival" failure is a chat that reaches Claude but can't actually see your home. The integration checks for this on every status poll and raises a repair with the exact fix when it finds:
- Claude isn't logged in (the app is up but unauthenticated);
- Claude has no Home Assistant token to read your home;
- the Model Context Protocol Server integration is missing or unreachable;
- Home Assistant refuses the app's token on the Model Context Protocol Server (a revoked token, or a non-administrator token while the server requires an administrator account) — needs an app that reports it;
- nothing is exposed to Assist (so there's nothing to see or control).
Separately, a sign-in that has expired is raised the moment a chat turn
hits it, or from the next status poll, whichever comes first — the shared
subscription token's refresh window is short, so an app asked only rarely
can lose it between chats. The repair and the chat reply both point at the
configured engine's own fix: for the Claude Code app, sign in again
(/login) in its console, or set a claude setup-token token as its OAuth
Token option; for the Codex app, run codex logout then codex login in
a console shell tab, or set an API key as its API Key option. Either durable
option survives idle spells. It clears once the app reports the sign-in
is good again.
These checks cost nothing (they read the status poll, not Claude). Press the
Check Claude health button (button.claude_code_check_claude_health) to run a
deeper probe — a tiny read that confirms
Claude can actually reach the MCP server right now. Needs the app ≥ 1.14.0 for
the reachability signal.
claude_ha.setup_voice turns local voice chat with Claude into one action. Give
it a language and it installs and starts the official Whisper (speech-to-text)
and Piper (text-to-speech) apps, then creates an Assist pipeline whose
conversation agent is Claude (it also confirms the Wyoming prompts for those two
apps and dismisses any duplicate prompt, so nothing is left to click). Whisper
and Piper already set up in Wyoming — by discovery or by hand with the app's
internal address — are reused; if their Wyoming entry is disabled, the action says so:
action: claude_ha.setup_voice
data:
language: uk # uk, pl, en, de, …
# tts_voice: uk_UA-ukrainian_tts-medium # override the default voice
# stt_model: small-int8 # set the Whisper model (default: keep the app's)Running it again updates the pipeline it created (the integration remembers it)
instead of adding another one; a pipeline you made yourself is never modified,
even if it has the same name. Then pick the new pipeline on your phone. Honest caveats: Whisper on CPU takes a
few seconds per phrase, and some voices (Ukrainian especially) are weaker than
others — pass tts_voice or switch to Cloud TTS if the default doesn't fit. The
action returns the engines it wired and the pipeline id (request the response
with response_variable to read it).
The security-critical work lives in the app; this integration deliberately does the least it can.
- Bearer token, not network trust. Every request carries a token issued via Supervisor discovery. It never appears in the UI and is redacted from diagnostics.
- Read-only by default. Chat is treated as untrusted input and runs deny-by-default; state changes require explicit, scoped, confirmed intents.
- What leaves Home Assistant. The integration sends these to the local app:
your message, the conversation id, the language, which surface it came from, the facts you chose
to remember, and — only if you turned on camera vision — the id of the camera
to look at. The integration itself attaches no entity states. The app then sends
the prompt to Claude (Anthropic) to answer it.
Claude reads and controls only the entities you have exposed to Assist, and
anything that changes state waits for your
yesunless it is judged benign. - No cloud calls from the integration. It only talks to the local app; the app is what calls Claude.
See SECURITY.md for the reporting policy, and the app for the full picture (env-scrubbed child processes, per-call statelessness, rate limiting, output redaction and audit logging).
The status sensor is refreshed by a DataUpdateCoordinator that polls the
app's /api/status endpoint every 60 seconds. Prompts (chat turns and the
ask action) are sent on demand.
- Requires Home Assistant OS / Supervised — the app is a Supervisor app and is not available on Home Assistant Container or Core installs.
- One Claude instance per Home Assistant (one app → one config entry).
- A remembered fact is sent with every chat turn as background for Claude, so it counts towards what the app processes; keep facts short.
- Entities unavailable for a few seconds after an app update or restart. Expected: while the app restarts its entities are unavailable and the log carries one INFO line, not an error; they come back within seconds of the app answering.
- "The Claude Code app is not running". Raised when the app has been stopped for 10 minutes. The integration does not start the app on its own: the repair offers to start it, or start it from the app page. Home Assistant retries setup automatically once it runs.
- "The app runs a different AI engine". The entry was set up for one engine and its app now reports another. Restore the app the entry was set up for, or delete the entry and add the app again.
- Chat suddenly only says it couldn't finish, with no reason given. The app hasn't told this integration why yet (an older app reports every failed model call the same generic way). Update the app; a current one names an expired sign-in specifically and raises the repair above instead.
- A request is refused with a reason. "The request is too large" means the
prompt or request exceeds the app's size limit (shown when the app
publishes one); shorten it. "Can't make changes because it has no Home
Assistant token" means the app options need a token before
mode: writeworks. "Needs your explicit confirmation" means the action must be confirmed before it runs. "Took too long to answer" is a timeout; try again. - Setup keeps retrying. Check that the app is installed, started and healthy; the status endpoint must be reachable on the internal network.
- Diagnostics. Download diagnostics from the integration's device page (the token is redacted) to inspect the last known status.
Delete the Claude integration from Settings → Devices & services. If Home Assistant installed the Claude Code app for you, remove it separately under Settings → Apps.
python3.14 -m venv .venv && source .venv/bin/activate
pip install -r requirements_test.txt
python scripts/install_component_requirements.py
pre-commit install
ruff check custom_components tests scripts
ruff format --check custom_components tests scripts
mypy custom_components/claude_ha
pytest --cov=custom_components.claude_ha --cov-report=term-missing
python scripts/secret_scan.py .
docker run --rm -v "$PWD":/github/workspace ghcr.io/home-assistant/hassfestCI runs hassfest, HACS validation, ruff, mypy, the test suite (100% coverage) and a secret scan on every push and pull request. See CONTRIBUTING.md.
Brand images live under custom_components/claude_ha/brand/ and are served by
Home Assistant's Brands Proxy API (2026.3+) — no separate brands submission
needed.
MIT © LayerTM
