Repository navigation
3.3.0: security review fixes - #2
Merged
Merged
Conversation
…min keys The Claude Code proxy is deployed into Scripts/ with the live access key in it, and read_script searched Scripts/, so a read key could read the admin key. Every script tool that resolves a name now refuses the proxy (by name, case-blind, and by file identity, so a link to it is caught), and list_python_scripts leaves it out. For a key without admin, read_script, get_automation, query_event_log, investigate_event and the log/trigger/schedule resources now have every known secret value blanked, failing closed if the values cannot be read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XbNsZWae49j96wmP8ip2us
send_email sends to any address the caller names, which is data leaving the house, so it is now admin like the other outbound tools. Pushover delivers only to the owner's own devices, so send_notification stays a write, and its description now says so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XbNsZWae49j96wmP8ip2us
A multi-action tool can now declare actions that need a higher scope than the tool (action_scopes) and actions that go through the delete gate (destructive_actions); the registry checks the names against the tool's action enum at import and writes the rule into the description. device_control stays a write tool, but reset_energy, which zeroes a kWh total that cannot be restored, now needs admin. zwave enter_exclusion, which removes hardware from the network, now needs confirm=true and the delete preference. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XbNsZWae49j96wmP8ip2us
executeAction(waitUntilDone=True) ran on Indigo's web-server thread with no deadline, so a slow or hung plugin action held every dashboard and tool call. It now runs on a short-lived thread joined for at most exec_lock.MAX_WAIT_SECONDS, the same pattern as plugin-provided tools; an action still going then is reported as timed out and possibly still running, and its outcome is logged when it lands. Claude Bridge's own plugin id, given directly or through its device, is refused: the action would run inside the plugin serving the request. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XbNsZWae49j96wmP8ip2us
The limits count calls per access key, and an admin key gets ten times them; with no scopes.json every key is admin, so a stock install enforces 1,200 a minute and 50,000 a day while Configure and /health showed 120 and 5,000. Enforcement is unchanged. /health now reports the configured figures, the multiplier, the effective limits by kind of key, whether every key is admin, and the limits each key is held to; Configure, the Configure-save log line and docs/configuration.md say the same, and the "per-session" wording is gone. /health named the configured keys and their scopes and listed recent tool calls to any authenticated key. Only an admin key now gets that; any other key gets the basic status and its own limits. With no scopes.json every key is admin, so nothing changes for those installs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XbNsZWae49j96wmP8ip2us
The endpoint answered every session problem and every notification with HTTP 200. It now sends what the 2025-03-26 and 2025-06-18 transports require: 404 for a session id it does not know, 400 for a missing one or an unsupported MCP-Protocol-Version, and 202 Accepted with no body for a notification or client response it accepts (400 for one that is not JSON-RPC). The JSON-RPC error body is still there. The empty-store grace after a restart is kept, so a client that never re-initializes carries on. The bundled proxy re-handshakes and replays once on the 404 or the session 400, as it did for the old -32600 (still recognised), and treats the empty 202 as the acknowledgement. Tested end to end against the real handler, across a simulated plugin restart. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XbNsZWae49j96wmP8ip2us
The MCP transport requires a server to validate Origin against DNS rebinding. A request whose Origin names a host other than this Mac (localhost, loopback, its own names and addresses) or the Indigo web server (the address Indigo reports, and its Reflector) now gets 403. A request with no Origin, which is every client that is not a browser, is unaffected. The Host header is deliberately not trusted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XbNsZWae49j96wmP8ip2us
list_triggers and list_schedules were cached, but a trigger or schedule changed outside Claude Bridge never reached the cache: the plugin subscribes to device, variable and action-group changes only. The trigger and schedule subscription and callback names could not be confirmed from the API baseline or the codebase, so the two lists are simply not cached any more (both are cheap). audit(check="home") counts disabled triggers and schedules but did not read those buckets, so the check straight after set_enabled served the old count for the whole TTL. It now reads them and set_enabled drops it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XbNsZWae49j96wmP8ip2us
…host:8176 client_setup now writes the scheme, host and port from indigo.server.getWebServerURL() into the deployed proxy alongside the token (the plugin reads it with the same error handling as get_web_server_url, and passes "" when Indigo cannot say). A URL that names this Mac (loopback, its own host name, or an address on one of its interfaces) keeps the host "localhost"; with no usable URL the old http://localhost:8176 stands. The proxy speaks HTTPS when the web server does; over loopback it does not check the certificate, which names the Mac, never "localhost", and to any other host it verifies as normal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XbNsZWae49j96wmP8ip2us
Delivery connected only to the first address vet_url returned, so a host with an unreachable first address (IPv6 on a network without it, say) failed every time. Every address vet_url returns has passed the egress firewall (it refuses the URL outright if any does not), and they are now tried in turn until one accepts the connection, all within the one delivery deadline, with the socket still pinned to the address that was checked. Only a failure to connect moves on; once connected, the request may have been delivered, so it is not repeated against another address. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XbNsZWae49j96wmP8ip2us
A required prompt argument that was absent or blank was filled with a "<device>" style placeholder, which sent Claude looking for a device literally called that. prompts/get now answers JSON-RPC -32602 naming the missing argument. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XbNsZWae49j96wmP8ip2us
… sees - security.md and the README safety block no longer say only the conversation with Claude leaves the Mac: e-mail, Pushover and webhooks do too, and say which keys can send them. - They no longer claim everything that opens the house needs admin. A write key can run any action group and fire any trigger or schedule, whatever they contain; a new section says so and advises giving phones a read key. - "What a read-only key can see" now covers scripts in both folders (the deployed proxy excepted), embedded automation scripts and the event log (redacted for keys without admin), and device history, which is SQLite SQL Logger only; also the admin-only /health detail, the Origin check, the exclusion gate and reset_energy needing admin. - architecture.md gave the wait_seconds default as eight; it is 5 (exec_lock.DEFAULT_WAIT_SECONDS). - device_history's description and what-it-does.md say it reads the SQLite SQL Logger only; the admin tier's blurb in the tool reference adds data leaving the house. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XbNsZWae49j96wmP8ip2us
Version bump: Info.plist PluginVersion, the plugin.py header and the README, with the 3.3.0 entry at the top of docs/changelog.md and the same text under the README's What's new, which now carries 3.3.0, 3.2.1 and 3.2.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XbNsZWae49j96wmP8ip2us
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes every finding from a fresh review of 3.2.1. Each fix is its own commit; the last one bumps the version to 3.3.0 and adds the changelog entry.
Security
read_scriptsearchedScripts/, where the deployedindigo_mcp_proxy.pycarries the live bearer token. No script tool can now read, list, write, rename, delete or run the proxy; the name check ignores case and also catches links to the file. For callers without admin,read_script,get_automation,query_event_log,investigate_eventand the log, trigger and schedule resources have known secret values blanked. The newredact_outputregistry flag does this. If the secret values can't be loaded, the reply is withheld rather than sent unredacted.send_emailnow needs admin, because it is data leaving the house.device_control reset_energy, which can't be undone, now needs admin.zwave enter_exclusiongoes behind the delete gate. These use new per-action registry rules,action_scopesanddestructive_actions, which are checked against each tool's action list at load./health: key names, scopes and recent calls are shown to admin keys only.Originisn't this Mac or the Indigo server (newsecurity/origin_guard.py). A request with no Origin, such as the proxy, mcp-remote or curl, is unaffected.Robustness
execute_device_actionwaits at most 20 s, on its own thread. It refuses to target Claude Bridge itself.api_baseline.py.auditnow drops its cache afterset_enabled.getWebServerURL(), so a non-default port or HTTPS works.prompts/getwith a missing required argument returns -32602 naming it.Docs
scopes.json./healthshows the effective limits. Enforcement is unchanged.security.mdand the README now name everything that leaves the Mac (email, Pushover, webhooks). They also say what a write key can do, and what a read key can see.architecture.mdgives the wait default as 5, not eight.device_historyis described as SQLite SQL Logger only.Behaviour changes for existing users
zwave enter_exclusionneedsconfirm=trueand the delete preference turned on./health.Origingets 403.localhost, and the traffic never leaves the machine. To any other host the certificate is verified.Test plan
python3 -m pytest -q: 1520 passed, 6 skipped (was 1431). The new tests exercise the behaviour, and several were confirmed to fail with the fix removed.ruff check .is clean.scripts/generate_tool_doc.py --checkpasses.Originheader through to the plugin, and that Claude Code keeps working across a plugin restart with the 202/404 responses.🤖 Generated with Claude Code
https://claude.ai/code/session_01XbNsZWae49j96wmP8ip2us
Generated by Claude Code