A Home Assistant CLI built for people, scripts, and coding agents. Inspect state, call services, and safely manage automations, scripts, and scenes from one consistent interface.
Ask your coding agent:
Read this repository and install the CLI.
Or choose one of the two installation paths below. Both require Python 3.11 or newer.
curl -LsSf https://raw.githubusercontent.com/jeremychan/ha-agent-cli/main/install.sh | sh
hash -r
ha-agent --versionThe installer installs the current main branch into an isolated environment under ~/.local/share/ha-agent-cli and links ha-agent into ~/.local/bin. It does not use sudo, modify the system Python, or require a published PyPI package.
hash -r clears any older ha-agent location cached by the current shell.
To inspect the script before running it:
curl -LsSf https://raw.githubusercontent.com/jeremychan/ha-agent-cli/main/install.sh -o install.sh
less install.sh
sh install.shRun the curl command again to upgrade to the latest main. To install a specific source archive instead, set HA_AGENT_CLI_SOURCE:
curl -LsSf https://raw.githubusercontent.com/jeremychan/ha-agent-cli/main/install.sh |
HA_AGENT_CLI_SOURCE=https://github.com/jeremychan/ha-agent-cli/archive/refs/tags/v0.1.0.zip shTo uninstall:
rm -rf ~/.local/share/ha-agent-cli
rm ~/.local/bin/ha-agentClone the repository and install it into a virtual environment:
git clone https://github.com/jeremychan/ha-agent-cli.git
cd ha-agent-cli
python3 -m venv .venv
. .venv/bin/activate
python -m pip install .
ha-agent infoThe virtual environment keeps the CLI and its dependencies isolated. Activate it again with . .venv/bin/activate whenever you want to use ha-agent, or follow the development setup below if you plan to contribute.
- Safe configuration changes: create, update, delete, and export automations, scripts, and scenes
- Dry-run by default: every mutation needs
--confirmbefore it touches Home Assistant - Conflict protection: configuration hashes stop one agent or shell session from overwriting another
- Verified writes and backups: updates are read back after saving, with the previous config stored locally
- Consistent output: every command returns an
{ok, command, data, meta}envelope in JSON or TOON - Focused queries: fuzzy search, field selection, and result limits keep large instances manageable
- Schema introspection: inspect service fields over WebSocket before constructing a call
- Agent integration: install the bundled skill file into Claude, Codex, Cursor, or OpenCode
ha-agent-cli uses Long-Lived Access Tokens to talk to your Home Assistant instance.
- Open your Home Assistant instance in a browser
- Go to your Profile (click your avatar in the bottom-left corner)
- Scroll to Long-Lived Access Tokens at the bottom
- Click Create Token, give it a name (e.g., "ha-agent-cli")
- Copy the token. It's shown once and cannot be retrieved later.
# Option 1: Save to config file (~/.config/ha-agent-cli/config.toml)
ha-agent setup --url http://homeassistant.local:8123 --token YOUR_TOKEN
# Option 2: Environment variables (best for CI/agents)
export HOME_ASSISTANT_URL=http://homeassistant.local:8123
export HOME_ASSISTANT_TOKEN=YOUR_TOKEN
# Option 3: Per-command flags (highest priority)
ha-agent --url http://homeassistant.local:8123 --token YOUR_TOKEN infoResolution order: CLI flags > environment variables > config file > defaults.
# Test your connection
ha-agent setup --checkThe examples below show both the command and its response. Entity IDs will differ on your instance, and timestamps, hashes, and backup paths change on every run. Long state attribute maps are shortened for readability.
Global output options go before the command. Here, fuzzy search finds kitchen lights, returns only useful fields, and stops after two matches:
$ ha-agent --fields entity_id,state,name --max-results 2 entity search "kitchen light"
{
"ok": true,
"command": "entity.search",
"data": [
{
"entity_id": "light.kitchen_ceiling",
"state": "on",
"name": "Kitchen ceiling"
},
{
"entity_id": "light.kitchen_cabinets",
"state": "off",
"name": "Kitchen cabinets"
}
],
"meta": {
"timestamp": "2026-07-18T14:22:31+00:00",
"duration_ms": 0
}
}The same response is smaller in TOON, which is useful when another tool or agent consumes it:
$ ha-agent --format toon --fields entity_id,state,name --max-results 2 entity search "kitchen light"
ok: true
command: entity.search
data: 2[entity_id,state,name]:
light.kitchen_ceiling,on,Kitchen ceiling
light.kitchen_cabinets,off,Kitchen cabinets
meta: timestamp: 2026-07-18T14:22:31+00:00
duration_ms: 0Mutating commands are dry-runs unless --confirm is present:
$ ha-agent service call light.turn_on \
--data '{"entity_id":"light.kitchen_ceiling","brightness_pct":70}'
{
"ok": true,
"command": "service.call",
"data": {
"dry_run": true,
"service": "light.turn_on",
"data": {
"entity_id": "light.kitchen_ceiling",
"brightness_pct": 70
}
},
"meta": {
"timestamp": "2026-07-18T14:25:08+00:00",
"duration_ms": 0
}
}Review the target and payload, then send the same call:
$ ha-agent service call light.turn_on \
--data '{"entity_id":"light.kitchen_ceiling","brightness_pct":70}' \
--confirm
{
"ok": true,
"command": "service.call",
"data": [],
"meta": {
"timestamp": "2026-07-18T14:25:16+00:00",
"duration_ms": 0
}
}An empty data list is a successful Home Assistant service response. Read the entity back if you need the resulting state:
$ ha-agent --format raw entity get light.kitchen_ceiling
{
"entity_id": "light.kitchen_ceiling",
"state": "on",
"attributes": {
"brightness": 179,
"friendly_name": "Kitchen ceiling"
},
"last_changed": "2026-07-18T14:25:16.281001+00:00",
"last_updated": "2026-07-18T14:25:16.281001+00:00"
}Service schemas come from the connected Home Assistant instance, so an agent does not need to guess field names:
$ ha-agent schema homeassistant.check_config
{
"ok": true,
"command": "schema",
"data": {
"service": "homeassistant.check_config",
"fields": {},
"description": "Checks the Home Assistant configuration files for errors."
},
"meta": {
"timestamp": "2026-07-18T14:27:40+00:00",
"duration_ms": 0
}
}Save the automation configuration as JSON. This example uses an entity-based trigger and restart mode so each new motion event resets the off timer:
{
"alias": "Kitchen motion light",
"description": "Turn on the kitchen light after dark, then turn it off two minutes after the last motion.",
"triggers": [
{
"trigger": "state",
"entity_id": "binary_sensor.kitchen_motion",
"to": "on"
}
],
"conditions": [
{
"condition": "sun",
"after": "sunset"
}
],
"actions": [
{
"action": "light.turn_on",
"target": {
"entity_id": "light.kitchen_ceiling"
},
"data": {
"brightness_pct": 70
}
},
{
"wait_for_trigger": [
{
"trigger": "state",
"entity_id": "binary_sensor.kitchen_motion",
"to": "off",
"for": "00:02:00"
}
]
},
{
"action": "light.turn_off",
"target": {
"entity_id": "light.kitchen_ceiling"
}
}
],
"mode": "restart"
}Assuming that file is named kitchen-motion.json, first ask for a mutation plan:
$ ha-agent automation create kitchen_motion --file kitchen-motion.json
{
"ok": true,
"command": "automation.create",
"data": {
"dry_run": true,
"operation": "create",
"resource": "automation",
"config_hash": null,
"before": null,
"after": {
"alias": "Kitchen motion light",
"description": "Turn on the kitchen light after dark, then turn it off two minutes after the last motion.",
"triggers": [
{
"trigger": "state",
"entity_id": "binary_sensor.kitchen_motion",
"to": "on"
}
],
"conditions": [
{
"condition": "sun",
"after": "sunset"
}
],
"actions": [
{
"action": "light.turn_on",
"target": {
"entity_id": "light.kitchen_ceiling"
},
"data": {
"brightness_pct": 70
}
},
{
"wait_for_trigger": [
{
"trigger": "state",
"entity_id": "binary_sensor.kitchen_motion",
"to": "off",
"for": "00:02:00"
}
]
},
{
"action": "light.turn_off",
"target": {
"entity_id": "light.kitchen_ceiling"
}
}
],
"mode": "restart"
},
"diff": [],
"warnings": [],
"issues": [],
"references": []
},
"meta": {
"timestamp": "2026-07-18T14:31:02+00:00",
"duration_ms": 0
}
}Nothing has changed yet. Add --confirm to create it and verify the saved configuration:
$ ha-agent automation create kitchen_motion --file kitchen-motion.json --confirm
{
"ok": true,
"command": "automation.create",
"data": {
"success": true,
"operation": "create",
"resource": "automation",
"config_hash": "sha256:bb7053c7f5190698f6320ed39f8ee00071c305244ca330c6138def742218ceea",
"backup_path": null,
"verified": true,
"issues": [],
"meta": {}
},
"meta": {
"timestamp": "2026-07-18T14:31:11+00:00",
"duration_ms": 0
}
}Exports contain the portable config and its current hash:
$ ha-agent --format raw automation export kitchen_motion > kitchen-motion.export.json
$ jq '{id, hash, alias: .config.alias}' kitchen-motion.export.json
{
"id": "kitchen_motion",
"hash": "sha256:bb7053c7f5190698f6320ed39f8ee00071c305244ca330c6138def742218ceea",
"alias": "Kitchen motion light"
}Edit .config in the export, write it to a new file, then use the exported hash as an optimistic lock:
$ jq '.config.actions[0].data.brightness_pct = 55 | .config' \
kitchen-motion.export.json > kitchen-motion.updated.json
$ HASH=$(jq -r .hash kitchen-motion.export.json)
$ ha-agent automation update kitchen_motion \
--file kitchen-motion.updated.json \
--config-hash "$HASH" \
--confirm
{
"ok": true,
"command": "automation.update",
"data": {
"success": true,
"operation": "update",
"resource": "automation",
"config_hash": "sha256:96c9bd4294332433e9f51cc14f5399e83393c730a72d293efea7a1de9061083b",
"backup_path": "/home/me/.local/state/ha-agent-cli/backups/http_homeassistant_local_8123/automation/kitchen_motion/20260718T143405Z.json",
"verified": true,
"issues": [],
"meta": {}
},
"meta": {
"timestamp": "2026-07-18T14:34:05+00:00",
"duration_ms": 0
}
}If the automation changed after the export, the update stops with a hash mismatch instead of overwriting the newer version. Updates and deletes also save the previous configuration under ~/.local/state/ha-agent-cli/backups/.
The same workflow applies to scripts and scenes:
$ ha-agent script export bedtime
$ ha-agent script update bedtime --file bedtime.json --config-hash "$HASH"
$ ha-agent scene create reading --file reading-scene.jsonThese commands are dry-runs as written. Add --confirm after reviewing the returned plan.
ha-agent
├── setup # Configure connection
├── info # HA instance info
├── entity
│ ├── list [--domain <d>] # List entities
│ ├── get <entity_id> # Get entity state
│ ├── search <query> # Fuzzy search entities
│ └── history <entity_id> # State history
├── service
│ ├── list [domain] # List services
│ ├── call <d.svc> -d '{...}' # Call service (dry-run, add --confirm)
│ └── schema <d.svc> # Service schema (WebSocket)
├── state
│ ├── list [domain] # Raw state dump
│ ├── get <entity_id> # Get state
│ └── set <entity_id> <val> # Set state (dry-run, add --confirm)
├── automation
│ ├── list # List automations
│ ├── get <entity_id> # Get runtime state
│ ├── config <id> # Get editable configuration
│ ├── create <id> # Create from JSON (dry-run)
│ ├── update <id> # Update with a config hash (dry-run)
│ ├── delete <id> # Delete with a config hash (dry-run)
│ ├── export <id> # Export config and hash
│ ├── trigger <entity_id> # Trigger (dry-run)
│ └── enable/disable <id> # Enable/disable (dry-run)
├── script
│ ├── list/get # Inspect scripts
│ └── create/update/delete/export
├── scene
│ ├── list/get # Inspect scenes
│ └── create/update/delete/export
├── event
│ └── fire <type> -d '{...}' # Fire event (dry-run)
├── log # Error log
├── schema <d.svc> # Schema shortcut
├── skill
│ ├── show # Print SKILL.md
│ └── install --target <agent> # Install into agent context
├── completion <shell> # Shell completion scripts
└── version # CLI + HA version
# JSON envelope (default)
ha-agent entity list
# Token-efficient TOON format (30-60% smaller)
ha-agent --format toon --fields entity_id,state entity list
# Raw HA API response (no envelope)
ha-agent --format raw entity list
# Field filtering + result limiting
ha-agent --fields entity_id,state,name --max-results 10 entity list| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | General error (API error, invalid input) |
| 2 | Usage error (bad flags, missing args) |
| 3 | Connection error (can't reach HA) |
| 4 | Auth error (invalid/expired token) |
# Show the skill file
ha-agent skill show
# Install for your coding agent
ha-agent skill install --target claude # → AGENTS.md
ha-agent skill install --target opencode # → AGENTS.md
ha-agent skill install --target cursor # → .cursor/rules/
ha-agent skill install --target codex # → .github/copilot-instructions.md
ha-agent skill install --target generic # → print to stdoutgit clone https://github.com/jeremychan/ha-agent-cli
cd ha-agent-cli
pip install -e ".[dev]"
ruff check .
ruff format --check .
mypy src/
pytest tests/unit/ tests/integration/ -v --tb=short --cov=srcMIT