feat: enhance security hooks for .env file access and add switchbot control and read skills
This commit is contained in:
parent
b54a747467
commit
81c756555d
@ -1,16 +1,30 @@
|
|||||||
{
|
{
|
||||||
"hooks": {
|
"hooks": {
|
||||||
"PreToolUse": [
|
"PreToolUse": [
|
||||||
|
{
|
||||||
|
"matcher": "Edit|Write|Read",
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "jq -r '.tool_input.file_path // empty' | { read -r f; if [[ \"$f\" == *.env && \"$f\" != *.env.example ]]; then echo '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"Refusing to read/edit a real .env file - it holds live secrets (HA_TOKEN, DISCORD_TOKEN, SwitchBot token/secret, etc). Use the corresponding .env.example instead.\"}}'; fi; }"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"matcher": "Edit|Write",
|
"matcher": "Edit|Write",
|
||||||
"hooks": [
|
"hooks": [
|
||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "jq -r '.tool_input.file_path // empty' | { read -r f; if [[ \"$f\" == *.env && \"$f\" != *.env.example ]]; then echo '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"Refusing to edit a real .env file - it holds live secrets (HA_TOKEN, DISCORD_TOKEN). Edit the corresponding .env.example instead.\"}}'; fi; }"
|
"command": "jq -r '.tool_input.file_path // empty' | { read -r f; if [[ \"$f\" == */gen/* ]]; then echo '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"gen/ is committed buf-generated protobuf/gRPC code. Edit the source .proto file under proto/ and run buf generate instead.\"}}'; fi; }"
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"matcher": "Bash",
|
||||||
|
"hooks": [
|
||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "jq -r '.tool_input.file_path // empty' | { read -r f; if [[ \"$f\" == */gen/* ]]; then echo '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"gen/ is committed buf-generated protobuf/gRPC code. Edit the source .proto file under proto/ and run buf generate instead.\"}}'; fi; }"
|
"command": "jq -r '.tool_input.command // empty' | { cmd=$(cat); reduced=$(echo \"$cmd\" | sed 's/\\.env\\.example//g'); if echo \"$reduced\" | grep -qE '\\.env'; then echo '{\"hookSpecificOutput\":{\"hookEventName\":\"PreToolUse\",\"permissionDecision\":\"deny\",\"permissionDecisionReason\":\"Refusing to run a shell command that references a real .env file - these hold live secrets and must never be read, printed, sourced, or exposed. Use .env.example, or ask the user to check the value directly.\"}}'; fi; }"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
87
.claude/skills/switchbot-control/SKILL.md
Normal file
87
.claude/skills/switchbot-control/SKILL.md
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
---
|
||||||
|
name: switchbot-control
|
||||||
|
description: Gated SwitchBot Cloud device control via the local reference CLI (tmp/reference/switchbot-control-reference) — turn-on/off, fan/AC/lock commands, and custom IR buttons. Always resolves and inspects the target first, always dry-runs before a real send, always requires explicit user approval before executing.
|
||||||
|
---
|
||||||
|
|
||||||
|
# SwitchBot Control (gated)
|
||||||
|
|
||||||
|
This wraps `tmp/reference/switchbot-control-reference/scripts/switchbotctl`,
|
||||||
|
the full control CLI. Every real control call fires a real signal at real
|
||||||
|
hardware (an actual IR blast, an actual lock toggle) — treat every non-dry-run
|
||||||
|
invocation as something you are asking permission to do, not something you
|
||||||
|
just do.
|
||||||
|
|
||||||
|
## Required sequence, every time
|
||||||
|
|
||||||
|
1. **Resolve the device.** Use the `switchbot-read` skill (`list all` /
|
||||||
|
`list remotes` / `list switches` / `list locks`) to find the exact
|
||||||
|
`deviceId` for the device the user named. Never guess an ID or reuse one
|
||||||
|
from memory without re-confirming it still matches — if more than one
|
||||||
|
device could match the name, ask which one; never pick one silently.
|
||||||
|
2. **Inspect the interface.** Run `switchbot-read interface DEVICE_ID` and
|
||||||
|
confirm the command you're about to send is actually in its documented
|
||||||
|
`commands` list (or, for a custom button, that the device is an infrared
|
||||||
|
remote where `customize`-type commands apply).
|
||||||
|
3. **Dry-run first.** Always construct and check the exact call with
|
||||||
|
`--dry-run` before ever running it for real:
|
||||||
|
```bash
|
||||||
|
cd tmp/reference/switchbot-control-reference
|
||||||
|
GOWORK=off ./scripts/switchbotctl ac DEVICE_ID 24 cool medium on --dry-run
|
||||||
|
```
|
||||||
|
Confirm the echoed `command`/`parameter`/`commandType` in the dry-run
|
||||||
|
output is exactly what you intend.
|
||||||
|
4. **Ask for explicit approval** before running the same command without
|
||||||
|
`--dry-run`. State the exact device (name + ID) and the exact action in
|
||||||
|
plain language. Do not treat a general "go ahead with the plan" from
|
||||||
|
earlier in the conversation as approval for a specific real control call —
|
||||||
|
ask again, per command, right before running it.
|
||||||
|
5. **Execute once.** Run the approved command exactly once. Do not retry
|
||||||
|
automatically on failure or on an ambiguous result — a repeated lock,
|
||||||
|
toggle, Bot press, or custom IR command can produce a real second action
|
||||||
|
(e.g. re-toggling something back off). If it needs retrying, ask first.
|
||||||
|
6. **Treat success as API acceptance, not physical confirmation.** SwitchBot
|
||||||
|
Cloud's infrared control is fire-and-forget — a `200`/`statusCode: 100`
|
||||||
|
response means the cloud accepted the command, not that the appliance
|
||||||
|
actually received or acted on the IR signal.
|
||||||
|
|
||||||
|
## Custom IR buttons (`button`) need the user, not a guess
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GOWORK=off ./scripts/switchbotctl button DEVICE_ID "Exact Button Name" --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
SwitchBot's API cannot enumerate a device's learned custom button names —
|
||||||
|
they only exist as case-sensitive labels in the SwitchBot app. Never
|
||||||
|
approximate, guess, or case-fold a button name. If you don't have the exact
|
||||||
|
label from the user, stop and ask for it rather than trying a plausible
|
||||||
|
guess.
|
||||||
|
|
||||||
|
## Command reference (see `tmp/reference/switchbot-control-reference/README.md` for the full list)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
switchbotctl turn-on DEVICE_ID [--dry-run]
|
||||||
|
switchbotctl turn-off DEVICE_ID [--dry-run]
|
||||||
|
switchbotctl press DEVICE_ID [--dry-run]
|
||||||
|
switchbotctl lock DEVICE_ID [--dry-run]
|
||||||
|
switchbotctl unlock DEVICE_ID [--dry-run]
|
||||||
|
switchbotctl fan DEVICE_ID swing|timer|lowSpeed|middleSpeed|highSpeed [--dry-run]
|
||||||
|
switchbotctl ac DEVICE_ID TEMPERATURE MODE FAN_SPEED POWER [--dry-run]
|
||||||
|
switchbotctl button DEVICE_ID "Exact Button Name" [--dry-run]
|
||||||
|
switchbotctl send DEVICE_ID COMMAND PARAMETER COMMAND_TYPE [--dry-run] # generic escape hatch
|
||||||
|
```
|
||||||
|
|
||||||
|
Always run from `tmp/reference/switchbot-control-reference/` with
|
||||||
|
`GOWORK=off` prefixed (this repo's root `go.work` doesn't include this nested
|
||||||
|
module — see the `switchbot-read` skill for why not to fix that by editing
|
||||||
|
`go.work`).
|
||||||
|
|
||||||
|
## Hard rules
|
||||||
|
|
||||||
|
- Never read, print, edit, source, or expose `.env` in this directory.
|
||||||
|
- Never add an automatic hook, cron, or script that calls `switchbotctl`
|
||||||
|
without a human in the loop for that specific invocation.
|
||||||
|
- This CLI is a **development/testing aid only** — it is not a dependency of
|
||||||
|
`ha-gateway`. The actual `RemoteService` implementation in `ha-gateway` has
|
||||||
|
its own hand-rolled SwitchBot HTTP client (see `plan.md`); use this CLI to
|
||||||
|
discover device IDs and verify command behavior during development, not as
|
||||||
|
something the running services call at runtime.
|
||||||
65
.claude/skills/switchbot-read/SKILL.md
Normal file
65
.claude/skills/switchbot-read/SKILL.md
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
---
|
||||||
|
name: switchbot-read
|
||||||
|
description: Read-only SwitchBot Cloud discovery via the local reference CLI (tmp/reference/switchbot-control-reference) — list devices/infrared remotes and inspect a device's documented command interface. Never sends control commands and never touches .env.
|
||||||
|
---
|
||||||
|
|
||||||
|
# SwitchBot Read-Only Discovery
|
||||||
|
|
||||||
|
Use this whenever you need to find a SwitchBot device ID, confirm a device's
|
||||||
|
type, or see which commands are documented for it — e.g. resolving "the
|
||||||
|
Dyson" or "the AC" to an exact `deviceId` before building or running any
|
||||||
|
control call in `ha-gateway`'s `RemoteService`.
|
||||||
|
|
||||||
|
This wraps `tmp/reference/switchbot-control-reference/scripts/switchbot-read`,
|
||||||
|
a wrapper that only accepts `list` and `interface` — it has no code path that
|
||||||
|
can send a device command, so it's safe to run freely.
|
||||||
|
|
||||||
|
## Why `GOWORK=off` is required
|
||||||
|
|
||||||
|
This reference tool is nested inside the `home-service` repo but is its own
|
||||||
|
Go module, not a member of the root `go.work`. Running it from inside the
|
||||||
|
workspace fails with "directory prefix . does not contain modules listed in
|
||||||
|
go.work" unless you disable workspace mode for that one subprocess:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd tmp/reference/switchbot-control-reference
|
||||||
|
GOWORK=off ./scripts/switchbot-read list all
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not add this tool to the root `go.work` — that file is committed, `tmp/`
|
||||||
|
is gitignored, and pointing a committed file at a gitignored path would break
|
||||||
|
every other clone of this repo.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
GOWORK=off ./scripts/switchbot-read list all
|
||||||
|
GOWORK=off ./scripts/switchbot-read list physical
|
||||||
|
GOWORK=off ./scripts/switchbot-read list remotes # infrared/virtual remotes — Dyson, AC live here
|
||||||
|
GOWORK=off ./scripts/switchbot-read list switches
|
||||||
|
GOWORK=off ./scripts/switchbot-read list locks
|
||||||
|
GOWORK=off ./scripts/switchbot-read interface DEVICE_ID
|
||||||
|
```
|
||||||
|
|
||||||
|
Output is JSON: `{filter, count, devices: [...]}` for `list`, or a single
|
||||||
|
`deviceView` object for `interface`. Each device includes a `commands` array
|
||||||
|
of documented `{name, parameter, commandType, description}` entries derived
|
||||||
|
from the device's type — this is a **static catalog**, not something
|
||||||
|
dynamically read from the appliance, and it does not include learned custom
|
||||||
|
IR button names (SwitchBot's API has no endpoint for that — see
|
||||||
|
`CustomButtonNamesDiscoverable: false` on infrared entries).
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
1. Never read, print, or expose `tmp/reference/switchbot-control-reference/.env`
|
||||||
|
yourself — the CLI reads it internally via `os.Getenv`, you never need to.
|
||||||
|
(A project hook already blocks `Read`/`Edit`/`Write` on real `.env` files
|
||||||
|
and Bash commands referencing them, but don't rely on the hook — just
|
||||||
|
don't try.)
|
||||||
|
2. This skill is discovery-only. If you need to send a command, use the
|
||||||
|
`switchbot-control` skill instead — never shell out to `switchbotctl`
|
||||||
|
(the full CLI) from here.
|
||||||
|
3. Treat `commands` in the output as "what SwitchBot's documented API
|
||||||
|
supports for this device type," not confirmation the physical appliance
|
||||||
|
will respond correctly to every listed command — some DIY/customized
|
||||||
|
remotes reject a documented standard command with error 160.
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,6 +1,7 @@
|
|||||||
# Environment files — never commit real tokens
|
# Environment files — never commit real tokens
|
||||||
.env
|
.env
|
||||||
*.env
|
*.env
|
||||||
|
tmp/
|
||||||
!.env.example
|
!.env.example
|
||||||
|
|
||||||
# Go build output
|
# Go build output
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user