{"id":"5cb6a8c2-1ab5-467f-91fe-e53d5eb5fc08","entityType":"agent","slug":"clawhub-chrischall-simplisafe-mcp","name":"simplisafe-mcp","canonicalUrl":"https://www.xpersona.co/agent/clawhub-chrischall-simplisafe-mcp","canonicalPath":"/agent/clawhub-chrischall-simplisafe-mcp","generatedAt":"2026-10-10T14:45:16.966Z","source":"CLAWHUB","claimStatus":"UNCLAIMED","verificationTier":"NONE","summary":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T12:03:05.666Z","emptyReason":null},"description":"Query and control a SimpliSafe alarm system from the shell with curl — read system state, sensors, locks, events and settings, and arm/disarm or lock/unlock. Use when the user asks about their SimpliSafe alarm, house sensors, door locks, or whether the system is armed. Requires a one-time browser login to mint a refresh token.","descriptionLabel":"Source description","evidenceSummary":"Capability contract not published. No trust telemetry is available yet. 1.4K downloads reported by the source. Last updated 10/10/2026.","installCommand":"clawhub skill install s17cjx1a349nz5apaqp02vgz4h85728z:simplisafe-mcp","sourceUrl":"https://clawhub.ai/chrischall/simplisafe-mcp","homepage":"https://clawhub.ai/chrischall/skills/simplisafe-mcp","primaryLinks":[{"label":"View on ClawHub","url":"https://clawhub.ai/chrischall/simplisafe-mcp","kind":"source"},{"label":"Homepage","url":"https://clawhub.ai/chrischall/skills/simplisafe-mcp","kind":"homepage"}],"safetyScore":84,"overallRank":62,"popularityScore":63,"trustScore":null,"claimedByName":null,"isOwner":false,"seoDescription":"simplisafe-mcp technical dossier on Xpersona with agent coverage, OPENCLEW support, and live trust metadata."},"coverage":{"evidence":{"source":"public-profile","verified":false,"confidence":"medium","updatedAt":"2026-10-10T12:03:05.666Z","emptyReason":null},"protocols":[{"protocol":"OPENCLEW","label":"OpenClaw","status":"self-declared","notes":"Declared in the public agent profile."}],"capabilities":[],"verifiedCount":0,"selfDeclaredCount":1,"capabilityMatrix":{"rows":[{"key":"OPENCLEW","type":"protocol","support":"unknown","confidenceSource":"profile","notes":"Listed on profile"}],"flattenedTokens":"protocol:OPENCLEW|unknown|profile"}},"adoption":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T12:03:05.666Z","emptyReason":null},"stars":null,"forks":null,"downloads":1445,"packageName":null,"latestVersion":"1.3.1","tractionLabel":"1.4K downloads"},"release":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T12:03:05.666Z","emptyReason":null},"lastUpdatedAt":"2026-10-10T12:03:05.666Z","lastCrawledAt":"2026-10-10T12:03:05.666Z","lastIndexedAt":null,"nextCrawlAt":"2026-10-11T12:03:05.666Z","lastVerifiedAt":null,"highlights":[{"version":"1.3.1","createdAt":"2026-10-09T23:27:24.696Z","changelog":"- Removed the file skill-card.md. - No changes were made to core functionality or usage documentation. - This update affects only project documentation.","fileCount":5,"zipByteSize":9767},{"version":"1.3.0","createdAt":"2026-10-07T13:40:55.399Z","changelog":"- Removed the file: skill-card.md. - No changes to core functionality or documentation in SKILL.md.","fileCount":5,"zipByteSize":9806},{"version":"1.2.4","createdAt":"2026-10-05T02:51:54.524Z","changelog":"- Removed the file skill-card.md. - No changes to functionality or documentation other than this file removal.","fileCount":5,"zipByteSize":9788},{"version":"1.2.3","createdAt":"2026-10-03T01:43:44.705Z","changelog":"- Removed the file: skill-card.md - No changes to functionality or documentation in SKILL.md - Housekeeping update only; no user-facing changes","fileCount":5,"zipByteSize":9669},{"version":"1.2.2","createdAt":"2026-09-30T16:56:12.655Z","changelog":"- Removed the skill-card.md file, which previously described the skill's summary and usage. - No changes to the skill's core logic, code, or user-facing commands. - Documentation and usage instructions remain unchanged.","fileCount":5,"zipByteSize":9705},{"version":"1.2.1","createdAt":"2026-09-25T15:55:21.906Z","changelog":"- Removed the file skill-card.md. - No changes to logic or documentation content; this is a minor cleanup release.","fileCount":5,"zipByteSize":9738},{"version":"1.2.0","createdAt":"2026-09-24T15:12:30.934Z","changelog":"- removed 1 file(s). - Updated SKILL.md and bundle contents.","fileCount":5,"zipByteSize":9849},{"version":"1.1.3","createdAt":"2026-09-23T21:41:37.440Z","changelog":"- Removed the file skill-card.md. - No changes to functionality or documentation other than file deletion.","fileCount":5,"zipByteSize":9705}]},"execution":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No published capability contract is available yet."},"installCommand":"clawhub skill install s17cjx1a349nz5apaqp02vgz4h85728z:simplisafe-mcp","setupComplexity":"low","setupSteps":["Install using `clawhub skill install s17cjx1a349nz5apaqp02vgz4h85728z:simplisafe-mcp` in an isolated environment before connecting it to live workloads.","No published capability contract is available yet, so validate auth and request/response behavior manually.","Review the upstream CLAWHUB listing at https://clawhub.ai/chrischall/simplisafe-mcp before using production credentials."],"contract":{"contractStatus":"missing","authModes":[],"requires":[],"forbidden":[],"supportsMcp":false,"supportsA2a":false,"supportsStreaming":false,"inputSchemaRef":null,"outputSchemaRef":null,"dataRegion":null,"contractUpdatedAt":null,"sourceUpdatedAt":null,"freshnessSeconds":null},"invocationGuide":{"preferredApi":{"snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-simplisafe-mcp/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-simplisafe-mcp/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-simplisafe-mcp/trust"},"curlExamples":["curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-simplisafe-mcp/snapshot\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-simplisafe-mcp/contract\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-simplisafe-mcp/trust\""],"jsonRequestTemplate":{"query":"summarize this repo","constraints":{"maxLatencyMs":2000,"protocolPreference":["OPENCLEW"]}},"jsonResponseTemplate":{"ok":true,"result":{"summary":"...","confidence":0.9},"meta":{"source":"CLAWHUB","generatedAt":"2026-10-10T14:45:16.963Z"}},"retryPolicy":{"maxAttempts":3,"backoffMs":[500,1500,3500],"retryableConditions":["HTTP_429","HTTP_503","NETWORK_TIMEOUT"]}},"endpoints":{"dossierUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-simplisafe-mcp/dossier","snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-simplisafe-mcp/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-simplisafe-mcp/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-simplisafe-mcp/trust"}},"reliability":{"evidence":{"source":"runtime-metrics","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No trust, reliability, or runtime telemetry is available."},"trust":{"status":"unavailable","handshakeStatus":"UNKNOWN","verificationFreshnessHours":null,"reputationScore":null,"p95LatencyMs":null,"successRate30d":null,"fallbackRate":null,"attempts30d":null,"trustUpdatedAt":null,"trustConfidence":"unknown","sourceUpdatedAt":null,"freshnessSeconds":null},"decisionGuardrails":{"doNotUseIf":["Contract metadata is missing or unavailable for deterministic execution."],"safeUseWhen":[],"riskFlags":["missing_or_unavailable_contract","trust_data_unavailable","schema_references_missing"],"operationalConfidence":"low"},"executionMetrics":{"observedLatencyMsP50":null,"observedLatencyMsP95":null,"estimatedCostUsd":null,"uptime30d":null,"rateLimitRpm":null,"rateLimitBurst":null,"lastVerifiedAt":null,"verificationSource":null},"runtimeMetrics":{"successRate":null,"avgLatencyMs":null,"avgCostUsd":null,"hallucinationRate":null,"retryRate":null,"disputeRate":null,"p50Latency":null,"p95Latency":null,"lastUpdated":null}},"benchmarks":{"evidence":{"source":"no-benchmark-data","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No benchmark suites or observed failure patterns are available."},"suites":[],"failurePatterns":[]},"artifacts":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T12:03:05.666Z","emptyReason":null},"readme":"Skill: simplisafe-mcp\n\nOwner: chrischall\n\nSummary: Query and control a SimpliSafe alarm system from the shell with curl — read system state, sensors, locks, events and settings, and arm/disarm or lock/unlock. Use when the user asks about their SimpliSafe alarm, house sensors, door locks, or whether the system is armed. Requires a one-time browser login to mint a refresh token.\n\nTags: latest:1.3.1\n\nVersion history:\n\nv1.3.1 | 2026-10-09T23:27:24.696Z | auto\n\n- Removed the file skill-card.md.\n- No changes were made to core functionality or usage documentation.\n- This update affects only project documentation.\n\nv1.3.0 | 2026-10-07T13:40:55.399Z | auto\n\n- Removed the file: skill-card.md.\n- No changes to core functionality or documentation in SKILL.md.\n\nv1.2.4 | 2026-10-05T02:51:54.524Z | auto\n\n- Removed the file skill-card.md.\n- No changes to functionality or documentation other than this file removal.\n\nv1.2.3 | 2026-10-03T01:43:44.705Z | auto\n\n- Removed the file: skill-card.md\n- No changes to functionality or documentation in SKILL.md\n- Housekeeping update only; no user-facing changes\n\nv1.2.2 | 2026-09-30T16:56:12.655Z | auto\n\n- Removed the skill-card.md file, which previously described the skill's summary and usage.\n- No changes to the skill's core logic, code, or user-facing commands.\n- Documentation and usage instructions remain unchanged.\n\nv1.2.1 | 2026-09-25T15:55:21.906Z | auto\n\n- Removed the file skill-card.md.\n- No changes to logic or documentation content; this is a minor cleanup release.\n\nv1.2.0 | 2026-09-24T15:12:30.934Z | auto\n\n- removed 1 file(s).\n- Updated SKILL.md and bundle contents.\n\nv1.1.3 | 2026-09-23T21:41:37.440Z | auto\n\n- Removed the file skill-card.md.\n- No changes to functionality or documentation other than file deletion.\n\nv1.1.2 | 2026-09-23T15:39:31.562Z | auto\n\n- Removed the unnecessary skill-card.md file.\n- No changes made to functionality or documentation.\n\nv1.1.1 | 2026-09-21T04:13:09.215Z | auto\n\n- Removed the file: skill-card.md\n- No changes to skill functionality or documentation aside from file removal.\n\nv1.1.0 | 2026-09-20T02:51:08.854Z | auto\n\n- Removed the file: skill-card.md\n- No changes to core functionality; documentation and skill usage remain the same.\n\nv1.0.0 | 2026-09-19T11:20:14.284Z | auto\n\n- Removed sample file: skill-card.md.\n- No functional or user-facing changes; this update only deletes documentation.\n\nv0.2.2 | 2026-09-15T19:25:22.693Z | auto\n\n- Removed the file: skill-card.md.\n- No functional or documentation changes to the skill code or description.\n\nv0.2.1 | 2026-09-10T17:51:21.062Z | auto\n\n- Removed the file skill-card.md, which may have contained metadata for the skill.\n- No changes to code or functional behavior.\n- Documentation and skill usage remain unchanged.\n\nv0.2.0 | 2026-09-04T22:22:50.033Z | auto\n\n- Removed the file: skill-card.md.  \n- No changes to functionality or documentation content.\n- Maintenance update to clean up repository files.\n\nv0.1.3 | 2026-08-28T11:35:07.689Z | auto\n\n- Removed the file skill-card.md.\n- No other functionality or documentation changes in this release.\n\nv0.1.2 | 2026-08-09T21:02:31.630Z | auto\n\n- Removed the sample file `skill-card.md`.\n- No changes to functionality or documentation.\n- This version contains only a cleanup by deleting a sample file.\n\nv0.1.1 | 2026-07-28T16:49:20.707Z | auto\n\n- Removed the file: skill-card.md\n- No other changes or feature updates in this release\n\nv0.1.0 | 2026-07-28T16:26:02.749Z | auto\n\nInitial release of SimpliSafe shell integration.\n\n- Query and control a SimpliSafe alarm system using curl and jq.\n- Read system state, sensors, locks, events, and settings.\n- Arm/disarm the system and lock/unlock doors directly from the shell.\n- Requires a one-time browser login to mint and save a refresh token.\n- Includes detailed usage instructions, security notes, and verified endpoint references.\n\nArchive index:\n\nArchive v1.3.1: 5 files, 9767 bytes\n\nFiles: references/recipes.md (6629b), references/ss-helpers.sh (5486b), skill-card.md (2160b), SKILL.md (6268b), _meta.json (133b)\n\nFile v1.3.1:SKILL.md\n\n---\nname: simplisafe-api\ndescription: Query and control a SimpliSafe alarm system from the shell with curl — read system state, sensors, locks, events and settings, and arm/disarm or lock/unlock. Use when the user asks about their SimpliSafe alarm, house sensors, door locks, or whether the system is armed. Requires a one-time browser login to mint a refresh token.\n---\n\n# SimpliSafe from the shell\n\nSimpliSafe's API is reachable **server-side** — no browser bridge, no extension,\nno signed-in tab. Auth is a bearer token minted from a long-lived OAuth refresh\ntoken, so everything here is plain `curl` + `jq`.\n\nRequires `curl` and `jq`.\n\n## One-time setup\n\nSimpliSafe issues no API keys. The credential is an OAuth refresh token, minted\nby a browser login you do **once**:\n\n```bash\nnode ~/git/simplisafe-mcp/scripts/bootstrap-auth.mjs           # prints an authorize URL\n# sign in, then copy the com.simplisafe.mobile://... URL the browser fails to open\nnode ~/git/simplisafe-mcp/scripts/bootstrap-auth.mjs \"<that URL>\"\n```\n\nThat writes `SIMPLISAFE_REFRESH_TOKEN` to `~/git/simplisafe-mcp/.env` (mode 0600).\nSimpliSafe does **not** rotate refresh tokens, so this survives indefinitely —\nuntil you sign out of all devices in the SimpliSafe app, which revokes it.\n\nTo capture the code from the browser: open DevTools → Network → tick **Preserve\nlog** *before* signing in, then find the failed navigation to\n`com.simplisafe.mobile://…?code=…` and copy its link address. The code is\nsingle-use and expires in ~2 minutes.\n\n## Core pattern\n\n```bash\nsource ~/git/simplisafe-mcp/skills/simplisafe-api/references/ss-helpers.sh\n\nss_api GET /api/authCheck | jq            # who am I\nSID=$(ss_sid)                             # the single active system id\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" | jq\n```\n\n`ss_api <METHOD> <PATH> [JSON_BODY]` attaches auth, prints the body on stdout,\nand on a non-2xx prints to **stderr** and returns 1 — so a failure never looks\nlike an empty result. Access tokens are cached in `$TMPDIR` (0600) and re-minted\nonly when stale; the helpers never write to the repo's `.env`.\n\n## Resolve first\n\nAlmost every path needs a **system id (`sid`)**, and getting one takes two calls\n(`authCheck` → `userId`, then the subscriptions route). `ss_sid` does both, and\n**fails loudly if the account has more than one system** rather than guessing —\npass the sid explicitly in that case.\n\nNote two routing traps:\n\n- The system version that decides `ss3/` routing is at\n  `location.system.version`, **not** the top-level `systemVersion` (a different\n  number entirely).\n- **Events and doorlock control are NOT under the `ss3/` prefix.** Everything\n  else is.\n\n## Reads\n\n```bash\n# Is it armed? -> OFF | HOME | AWAY | HOME_COUNT | AWAY_COUNT | ALARM\nss_api GET \"/users/$(ss_api GET /api/authCheck | jq -r .userId)/subscriptions?activeOnly=true\" \\\n  | jq -r '.subscriptions[0].location.system.alarmState'\n\n# Anything wrong with a sensor?\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq '[.sensors[] | select(.flags.offline or .flags.lowBattery) | {name, type, flags}]'\n\n# Recent activity\nss_api GET \"/subscriptions/$SID/events?numEvents=10\" \\\n  | jq -r '.events[] | \"\\(.eventTimestamp|todate)  \\(.info)\"'\n```\n\nMore recipes, including lock state and base-station health:\n[references/recipes.md](references/recipes.md).\n\n## Writes — read this before running one\n\nThese commands act on a **physical security system**. Disarming leaves a house\nunmonitored; arming can trip a siren and a monitoring-center dispatch; unlocking\nopens a real door. Confirm intent with the user before running any of them, and\nnever run one speculatively.\n\n```bash\nss_api POST \"/ss3/subscriptions/$SID/state/away\"      # or /home, /off — no body\nss_api POST \"/doorlock/$SID/<serial>/state\" '{\"state\":\"lock\"}'   # or \"unlock\"\n```\n\n**A 2xx is not proof it worked.** Re-read and check one field:\n\n```bash\nsleep 3\nss_api GET \"/users/$(ss_api GET /api/authCheck | jq -r .userId)/subscriptions?activeOnly=true\" \\\n  | jq -r '.subscriptions[0].location.system.alarmState'\n```\n\nFour things that make naive verification lie:\n\n- Arming reports `AWAY_COUNT` / `HOME_COUNT` while the exit delay runs, settling\n  to `AWAY` / `HOME`. That is success in progress, not failure.\n- Do **not** diff whole objects — `alarmStateTimestamp`, `stateUpdated` and\n  `lastUpdated` advance on their own, so any call would look successful.\n- **Re-read locks with `forceUpdate=true`.** The cached payload lags by minutes\n  and has been seen reporting a *jammed* lock as cleanly `unlocked`.\n- **A lock takes ~5 s to report**, so a 3-second check calls a successful unlock\n  a failure. Poll, don't sleep once.\n\n**Arming also LOCKS your doors.** Each lock's `setting` has `home`/`away` flags\nthat auto-lock on arm, and `homeToOff`/`awayToOff` controlling whether disarming\nunlocks them. With `homeToOff: 0` (common), arm-then-disarm leaves the doors\nlocked — so it is *not* a safe way to test arming. Check with:\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq -r '.sensors[] | select(.type==16) | \"\\(.name)\\t\\(.setting)\"'\n```\n\nA lock reporting `lockJamState: 1` will not respond to commands at all: the POST\nstill returns `200`, and nothing moves. That is a hardware fault, not a bad\nrequest.\n\n## PINs are cleartext\n\n`/ss3/subscriptions/$SID/settings/normal` returns `settings.pins` — the master,\nduress and named-user **alarm codes, in cleartext**, in the same payload as the\nharmless settings. Project `.settings.normal` unless the user explicitly wants\nthe codes:\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/settings/normal?forceUpdate=false\" | jq '.settings.normal'\n```\n\n## Notes\n\n- `forceUpdate=true` makes the base station re-poll its devices — slower and\n  harder on the hardware. Leave it `false` unless freshness matters.\n- Lock state encoding is counter-intuitive: `lockState` **1 = locked, 2 =\n  unlocked**, and `lockJamState` overrides both.\n- Device type ids `21`, `23` and `24` appear on real systems but aren't in any\n  public enum — handle unknown types gracefully.\n- Avoid tight polling; the API rate-limits.\n\nFull verified endpoint reference: `~/git/simplisafe-mcp/docs/SIMPLISAFE-API.md`.\n\nFile v1.3.1:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"simplisafe-mcp\",\n  \"version\": \"1.3.1\",\n  \"publishedAt\": 1791588444696\n}\n\nFile v1.3.1:references/recipes.md\n\n# SimpliSafe curl + jq recipes\n\nAll examples assume:\n\n```bash\nsource ~/git/simplisafe-mcp/skills/simplisafe-api/references/ss-helpers.sh\nSID=$(ss_sid)\nUID_=$(ss_api GET /api/authCheck | jq -r .userId)\n```\n\nEvery recipe below was run against a live account.\n\n## System\n\n### Full system state\n\n```bash\nss_api GET \"/users/$UID_/subscriptions?activeOnly=true\" \\\n  | jq '.subscriptions[] | {\n      sid,\n      name: .location.locationName,\n      state: .location.system.alarmState,\n      alarming: .location.system.isAlarming,\n      offline: .location.system.isOffline,\n      powerOutage: .location.system.powerOutage,\n      conn: .location.system.connType,\n      version: .location.system.version\n    }'\n```\n\n### One-line \"is the house armed?\"\n\n```bash\nss_api GET \"/users/$UID_/subscriptions?activeOnly=true\" \\\n  | jq -r '.subscriptions[0].location.system | \"\\(.alarmState)\\(if .isAlarming then \"  ** ALARMING **\" else \"\" end)\"'\n```\n\n### Base-station messages (firmware notices, faults)\n\n```bash\nss_api GET \"/users/$UID_/subscriptions?activeOnly=true\" \\\n  | jq -r '.subscriptions[0].location.system.messages[] | \"\\(.timestamp|todate)  \\(.text)\"'\n```\n\n## Sensors\n\n### All devices, compact\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq -r '.sensors[] | \"\\(.type)\\t\\(.name)\\tlowBat=\\(.flags.lowBattery)\\toffline=\\(.flags.offline)\"' \\\n  | column -t\n```\n\n### Only devices needing attention\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq '[.sensors[]\n         | select(.flags.offline or .flags.lowBattery or (.status.triggered // false))\n         | {name, type, offline: .flags.offline, lowBattery: .flags.lowBattery, triggered: .status.triggered}]'\n```\n\n### Currently-open entry sensors (type 5)\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=true\" \\\n  | jq -r '.sensors[] | select(.type==5 and .status.triggered==true) | .name'\n```\n\n`forceUpdate=true` is warranted here — an open/closed reading is only meaningful\nif it is fresh.\n\n### Device type ids\n\n| id | device | | id | device |\n| --- | --- | --- | --- | --- |\n| 0 | remote | | 13 | siren |\n| 1 | keypad | | 14 | smoke + CO |\n| 2 | keychain | | 15 | doorbell |\n| 3 | panic button | | 16 | **lock** |\n| 4 | motion | | 17 | outdoor camera |\n| 5 | **entry** | | 20 | motion v2 |\n| 6 | glass break | | 22 | outdoor bell box |\n| 7 | carbon monoxide | | 253 | lock keypad |\n| 8 | smoke | | | |\n| 9 | leak | | 21, 23, 24 | seen live, **not in any public enum** |\n| 10 | temperature | | | |\n| 12 | camera | | | |\n\nType 24 carries `smokeTriggered` / `coTriggered` / `tamper` / `endOfLife`.\n\n## Locks\n\n### Lock roster with decoded state\n\n`lockState` **1 = locked, 2 = unlocked**; `lockJamState` overrides both.\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq -r '.sensors[] | select(.type==16) |\n      \"\\(.name)\\t\\(if .status.lockJamState==1 then \"JAMMED\"\n                   elif .status.lockState==1 then \"locked\"\n                   else \"unlocked\" end)\\tlowBat=\\(.status.lockLowBattery)\\tpinPadOffline=\\(.status.pinPadOffline)\"' \\\n  | column -t\n```\n\n### Serials, for the write commands\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq -r '.sensors[] | select(.type==16) | \"\\(.name)\\t\\(.serial)\"'\n```\n\n## Events\n\n### Recent activity, human-readable\n\n```bash\nss_api GET \"/subscriptions/$SID/events?numEvents=20\" \\\n  | jq -r '.events[] | \"\\(.eventTimestamp|todate)  [\\(.eventType)]  \\(.info)  \\(.sensorName // \"\")\"'\n```\n\nNote: **no `ss3/` prefix** on this route, and `eventTimestamp` is in seconds.\n\n> **`numEvents` maxes out at 50.** Verified by bisection against the live API:\n> 50 succeeds, 51 and above return `400 InvalidParameter`. To reach further back,\n> page with `fromTimestamp` rather than asking for more at once.\n\n### Only alarms\n\n```bash\nss_api GET \"/subscriptions/$SID/events?numEvents=50\" \\\n  | jq -r '.events[] | select(.eventType==\"alarm\") | \"\\(.eventTimestamp|todate)  \\(.info)\"'\n```\n\n### Since a point in time\n\n```bash\nSINCE=$(date -v-24H +%s)     # macOS; GNU: date -d '24 hours ago' +%s\nss_api GET \"/subscriptions/$SID/events?fromTimestamp=$SINCE&numEvents=50\" \\\n  | jq -r '.events[] | \"\\(.eventTimestamp|todate)  \\(.info)\"'\n```\n\n### Who disarmed it, and when\n\n```bash\nss_api GET \"/subscriptions/$SID/events?numEvents=50\" \\\n  | jq -r '.events[] | select(.info | test(\"Disarm\"; \"i\")) | \"\\(.eventTimestamp|todate)  \\(.info)  by \\(.pinName // \"unknown\")\"'\n```\n\n## Settings and health\n\n### Delays and volumes (PIN block excluded)\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/settings/normal?forceUpdate=false\" \\\n  | jq '.settings.normal | {entryDelayHome, entryDelayAway, exitDelayHome, exitDelayAway,\n                            alarmVolume, alarmDuration, doorChime, voicePrompts}'\n```\n\n### Base-station health\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/settings/normal?forceUpdate=false\" \\\n  | jq '.basestationStatus | {wifiStatus, wifiRssi, wallPower, backupBattery,\n                              gsmStatus, gsmRssi, rfJamming, cellCarrier}'\n```\n\n### PINs — cleartext, ask first\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/settings/normal?forceUpdate=false\" | jq '.settings.pins'\n```\n\nReturns the master and duress codes and every named user PIN in the clear. Only\nrun it if the user actually wants the codes; `.settings.normal` above is the\nsafe projection for everything else.\n\n## Writes\n\nConfirm with the user first — these move real hardware.\n\n```bash\n# Arm / disarm (no request body)\nss_api POST \"/ss3/subscriptions/$SID/state/away\"\nss_api POST \"/ss3/subscriptions/$SID/state/home\"\nss_api POST \"/ss3/subscriptions/$SID/state/off\"\n\n# Locks (serial from the roster above)\nss_api POST \"/doorlock/$SID/<serial>/state\" '{\"state\":\"lock\"}'\nss_api POST \"/doorlock/$SID/<serial>/state\" '{\"state\":\"unlock\"}'\n```\n\n### Verify, don't assume\n\n```bash\narm_and_verify() {\n  local want=\"$1\"\n  ss_api POST \"/ss3/subscriptions/$SID/state/$want\" >/dev/null || return 1\n  sleep 3\n  local got\n  got=$(ss_api GET \"/users/$UID_/subscriptions?activeOnly=true\" \\\n        | jq -r '.subscriptions[0].location.system.alarmState')\n  case \"$got\" in\n    \"$(echo \"$want\" | tr '[:lower:]' '[:upper:]')\")       echo \"confirmed: $got\" ;;\n    \"$(echo \"$want\" | tr '[:lower:]' '[:upper:]')_COUNT\") echo \"in progress (exit delay): $got\" ;;\n    *) echo \"UNCONFIRMED: wanted $want, system reports $got\" >&2; return 1 ;;\n  esac\n}\n```\n\nThe `_COUNT` case is the exit delay counting down — a success, not a failure.\nComparing whole objects instead of this one field would report success\nunconditionally, because the timestamps advance on their own.\n\nFile v1.3.1:skill-card.md\n\n## Description:\n\nHelps agents check SimpliSafe alarm, sensor, lock, and event status and issue alarm or lock commands through the shell.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nSimpliSafe account holders and their agents can inspect home security status and, with explicit authorization, arm or disarm the alarm and control door locks.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Authenticated commands can disarm the alarm or unlock doors without an enforced confirmation step.\n\nMitigation: Require explicit human confirmation before every arm, disarm, lock, or unlock action; prefer a version that enforces write confirmation and restricts allowed actions.\n\nRisk: A long-lived refresh token can grant ongoing account access if exposed in logs, shell history, or shared files.\n\nMitigation: Keep the token private and avoid exposing credentials in logs, shell history, or shared files.\n\nRisk: The settings response can contain cleartext alarm PINs, and lock status can be stale after a write.\n\nMitigation: Request PINs only with explicit consent; exclude them from routine settings output and refresh lock status before reporting a change as complete.\n\n## Reference(s):\n\n- [ClawHub skill release](https://clawhub.ai/chrischall/skills/simplisafe-mcp)\n- [SimpliSafe command recipes](references/recipes.md)\n- [SimpliSafe shell helpers](references/ss-helpers.sh)\n\n## Skill Output:\n\n**Output Type(s):** [Shell commands, Guidance, Configuration instructions]\n\n**Output Format:** [Markdown with shell commands and JSON-query examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Requires an authenticated SimpliSafe account, curl, and jq.]\n\n## Skill Version(s):\n\n1.3.1 (source: ClawHub release metadata)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v1.3.0: 5 files, 9806 bytes\n\nFiles: references/recipes.md (6629b), references/ss-helpers.sh (5486b), skill-card.md (2239b), SKILL.md (6268b), _meta.json (133b)\n\nFile v1.3.0:SKILL.md\n\n---\nname: simplisafe-api\ndescription: Query and control a SimpliSafe alarm system from the shell with curl — read system state, sensors, locks, events and settings, and arm/disarm or lock/unlock. Use when the user asks about their SimpliSafe alarm, house sensors, door locks, or whether the system is armed. Requires a one-time browser login to mint a refresh token.\n---\n\n# SimpliSafe from the shell\n\nSimpliSafe's API is reachable **server-side** — no browser bridge, no extension,\nno signed-in tab. Auth is a bearer token minted from a long-lived OAuth refresh\ntoken, so everything here is plain `curl` + `jq`.\n\nRequires `curl` and `jq`.\n\n## One-time setup\n\nSimpliSafe issues no API keys. The credential is an OAuth refresh token, minted\nby a browser login you do **once**:\n\n```bash\nnode ~/git/simplisafe-mcp/scripts/bootstrap-auth.mjs           # prints an authorize URL\n# sign in, then copy the com.simplisafe.mobile://... URL the browser fails to open\nnode ~/git/simplisafe-mcp/scripts/bootstrap-auth.mjs \"<that URL>\"\n```\n\nThat writes `SIMPLISAFE_REFRESH_TOKEN` to `~/git/simplisafe-mcp/.env` (mode 0600).\nSimpliSafe does **not** rotate refresh tokens, so this survives indefinitely —\nuntil you sign out of all devices in the SimpliSafe app, which revokes it.\n\nTo capture the code from the browser: open DevTools → Network → tick **Preserve\nlog** *before* signing in, then find the failed navigation to\n`com.simplisafe.mobile://…?code=…` and copy its link address. The code is\nsingle-use and expires in ~2 minutes.\n\n## Core pattern\n\n```bash\nsource ~/git/simplisafe-mcp/skills/simplisafe-api/references/ss-helpers.sh\n\nss_api GET /api/authCheck | jq            # who am I\nSID=$(ss_sid)                             # the single active system id\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" | jq\n```\n\n`ss_api <METHOD> <PATH> [JSON_BODY]` attaches auth, prints the body on stdout,\nand on a non-2xx prints to **stderr** and returns 1 — so a failure never looks\nlike an empty result. Access tokens are cached in `$TMPDIR` (0600) and re-minted\nonly when stale; the helpers never write to the repo's `.env`.\n\n## Resolve first\n\nAlmost every path needs a **system id (`sid`)**, and getting one takes two calls\n(`authCheck` → `userId`, then the subscriptions route). `ss_sid` does both, and\n**fails loudly if the account has more than one system** rather than guessing —\npass the sid explicitly in that case.\n\nNote two routing traps:\n\n- The system version that decides `ss3/` routing is at\n  `location.system.version`, **not** the top-level `systemVersion` (a different\n  number entirely).\n- **Events and doorlock control are NOT under the `ss3/` prefix.** Everything\n  else is.\n\n## Reads\n\n```bash\n# Is it armed? -> OFF | HOME | AWAY | HOME_COUNT | AWAY_COUNT | ALARM\nss_api GET \"/users/$(ss_api GET /api/authCheck | jq -r .userId)/subscriptions?activeOnly=true\" \\\n  | jq -r '.subscriptions[0].location.system.alarmState'\n\n# Anything wrong with a sensor?\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq '[.sensors[] | select(.flags.offline or .flags.lowBattery) | {name, type, flags}]'\n\n# Recent activity\nss_api GET \"/subscriptions/$SID/events?numEvents=10\" \\\n  | jq -r '.events[] | \"\\(.eventTimestamp|todate)  \\(.info)\"'\n```\n\nMore recipes, including lock state and base-station health:\n[references/recipes.md](references/recipes.md).\n\n## Writes — read this before running one\n\nThese commands act on a **physical security system**. Disarming leaves a house\nunmonitored; arming can trip a siren and a monitoring-center dispatch; unlocking\nopens a real door. Confirm intent with the user before running any of them, and\nnever run one speculatively.\n\n```bash\nss_api POST \"/ss3/subscriptions/$SID/state/away\"      # or /home, /off — no body\nss_api POST \"/doorlock/$SID/<serial>/state\" '{\"state\":\"lock\"}'   # or \"unlock\"\n```\n\n**A 2xx is not proof it worked.** Re-read and check one field:\n\n```bash\nsleep 3\nss_api GET \"/users/$(ss_api GET /api/authCheck | jq -r .userId)/subscriptions?activeOnly=true\" \\\n  | jq -r '.subscriptions[0].location.system.alarmState'\n```\n\nFour things that make naive verification lie:\n\n- Arming reports `AWAY_COUNT` / `HOME_COUNT` while the exit delay runs, settling\n  to `AWAY` / `HOME`. That is success in progress, not failure.\n- Do **not** diff whole objects — `alarmStateTimestamp`, `stateUpdated` and\n  `lastUpdated` advance on their own, so any call would look successful.\n- **Re-read locks with `forceUpdate=true`.** The cached payload lags by minutes\n  and has been seen reporting a *jammed* lock as cleanly `unlocked`.\n- **A lock takes ~5 s to report**, so a 3-second check calls a successful unlock\n  a failure. Poll, don't sleep once.\n\n**Arming also LOCKS your doors.** Each lock's `setting` has `home`/`away` flags\nthat auto-lock on arm, and `homeToOff`/`awayToOff` controlling whether disarming\nunlocks them. With `homeToOff: 0` (common), arm-then-disarm leaves the doors\nlocked — so it is *not* a safe way to test arming. Check with:\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq -r '.sensors[] | select(.type==16) | \"\\(.name)\\t\\(.setting)\"'\n```\n\nA lock reporting `lockJamState: 1` will not respond to commands at all: the POST\nstill returns `200`, and nothing moves. That is a hardware fault, not a bad\nrequest.\n\n## PINs are cleartext\n\n`/ss3/subscriptions/$SID/settings/normal` returns `settings.pins` — the master,\nduress and named-user **alarm codes, in cleartext**, in the same payload as the\nharmless settings. Project `.settings.normal` unless the user explicitly wants\nthe codes:\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/settings/normal?forceUpdate=false\" | jq '.settings.normal'\n```\n\n## Notes\n\n- `forceUpdate=true` makes the base station re-poll its devices — slower and\n  harder on the hardware. Leave it `false` unless freshness matters.\n- Lock state encoding is counter-intuitive: `lockState` **1 = locked, 2 =\n  unlocked**, and `lockJamState` overrides both.\n- Device type ids `21`, `23` and `24` appear on real systems but aren't in any\n  public enum — handle unknown types gracefully.\n- Avoid tight polling; the API rate-limits.\n\nFull verified endpoint reference: `~/git/simplisafe-mcp/docs/SIMPLISAFE-API.md`.\n\nFile v1.3.0:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"simplisafe-mcp\",\n  \"version\": \"1.3.0\",\n  \"publishedAt\": 1791380455399\n}\n\nFile v1.3.0:references/recipes.md\n\n# SimpliSafe curl + jq recipes\n\nAll examples assume:\n\n```bash\nsource ~/git/simplisafe-mcp/skills/simplisafe-api/references/ss-helpers.sh\nSID=$(ss_sid)\nUID_=$(ss_api GET /api/authCheck | jq -r .userId)\n```\n\nEvery recipe below was run against a live account.\n\n## System\n\n### Full system state\n\n```bash\nss_api GET \"/users/$UID_/subscriptions?activeOnly=true\" \\\n  | jq '.subscriptions[] | {\n      sid,\n      name: .location.locationName,\n      state: .location.system.alarmState,\n      alarming: .location.system.isAlarming,\n      offline: .location.system.isOffline,\n      powerOutage: .location.system.powerOutage,\n      conn: .location.system.connType,\n      version: .location.system.version\n    }'\n```\n\n### One-line \"is the house armed?\"\n\n```bash\nss_api GET \"/users/$UID_/subscriptions?activeOnly=true\" \\\n  | jq -r '.subscriptions[0].location.system | \"\\(.alarmState)\\(if .isAlarming then \"  ** ALARMING **\" else \"\" end)\"'\n```\n\n### Base-station messages (firmware notices, faults)\n\n```bash\nss_api GET \"/users/$UID_/subscriptions?activeOnly=true\" \\\n  | jq -r '.subscriptions[0].location.system.messages[] | \"\\(.timestamp|todate)  \\(.text)\"'\n```\n\n## Sensors\n\n### All devices, compact\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq -r '.sensors[] | \"\\(.type)\\t\\(.name)\\tlowBat=\\(.flags.lowBattery)\\toffline=\\(.flags.offline)\"' \\\n  | column -t\n```\n\n### Only devices needing attention\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq '[.sensors[]\n         | select(.flags.offline or .flags.lowBattery or (.status.triggered // false))\n         | {name, type, offline: .flags.offline, lowBattery: .flags.lowBattery, triggered: .status.triggered}]'\n```\n\n### Currently-open entry sensors (type 5)\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=true\" \\\n  | jq -r '.sensors[] | select(.type==5 and .status.triggered==true) | .name'\n```\n\n`forceUpdate=true` is warranted here — an open/closed reading is only meaningful\nif it is fresh.\n\n### Device type ids\n\n| id | device | | id | device |\n| --- | --- | --- | --- | --- |\n| 0 | remote | | 13 | siren |\n| 1 | keypad | | 14 | smoke + CO |\n| 2 | keychain | | 15 | doorbell |\n| 3 | panic button | | 16 | **lock** |\n| 4 | motion | | 17 | outdoor camera |\n| 5 | **entry** | | 20 | motion v2 |\n| 6 | glass break | | 22 | outdoor bell box |\n| 7 | carbon monoxide | | 253 | lock keypad |\n| 8 | smoke | | | |\n| 9 | leak | | 21, 23, 24 | seen live, **not in any public enum** |\n| 10 | temperature | | | |\n| 12 | camera | | | |\n\nType 24 carries `smokeTriggered` / `coTriggered` / `tamper` / `endOfLife`.\n\n## Locks\n\n### Lock roster with decoded state\n\n`lockState` **1 = locked, 2 = unlocked**; `lockJamState` overrides both.\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq -r '.sensors[] | select(.type==16) |\n      \"\\(.name)\\t\\(if .status.lockJamState==1 then \"JAMMED\"\n                   elif .status.lockState==1 then \"locked\"\n                   else \"unlocked\" end)\\tlowBat=\\(.status.lockLowBattery)\\tpinPadOffline=\\(.status.pinPadOffline)\"' \\\n  | column -t\n```\n\n### Serials, for the write commands\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq -r '.sensors[] | select(.type==16) | \"\\(.name)\\t\\(.serial)\"'\n```\n\n## Events\n\n### Recent activity, human-readable\n\n```bash\nss_api GET \"/subscriptions/$SID/events?numEvents=20\" \\\n  | jq -r '.events[] | \"\\(.eventTimestamp|todate)  [\\(.eventType)]  \\(.info)  \\(.sensorName // \"\")\"'\n```\n\nNote: **no `ss3/` prefix** on this route, and `eventTimestamp` is in seconds.\n\n> **`numEvents` maxes out at 50.** Verified by bisection against the live API:\n> 50 succeeds, 51 and above return `400 InvalidParameter`. To reach further back,\n> page with `fromTimestamp` rather than asking for more at once.\n\n### Only alarms\n\n```bash\nss_api GET \"/subscriptions/$SID/events?numEvents=50\" \\\n  | jq -r '.events[] | select(.eventType==\"alarm\") | \"\\(.eventTimestamp|todate)  \\(.info)\"'\n```\n\n### Since a point in time\n\n```bash\nSINCE=$(date -v-24H +%s)     # macOS; GNU: date -d '24 hours ago' +%s\nss_api GET \"/subscriptions/$SID/events?fromTimestamp=$SINCE&numEvents=50\" \\\n  | jq -r '.events[] | \"\\(.eventTimestamp|todate)  \\(.info)\"'\n```\n\n### Who disarmed it, and when\n\n```bash\nss_api GET \"/subscriptions/$SID/events?numEvents=50\" \\\n  | jq -r '.events[] | select(.info | test(\"Disarm\"; \"i\")) | \"\\(.eventTimestamp|todate)  \\(.info)  by \\(.pinName // \"unknown\")\"'\n```\n\n## Settings and health\n\n### Delays and volumes (PIN block excluded)\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/settings/normal?forceUpdate=false\" \\\n  | jq '.settings.normal | {entryDelayHome, entryDelayAway, exitDelayHome, exitDelayAway,\n                            alarmVolume, alarmDuration, doorChime, voicePrompts}'\n```\n\n### Base-station health\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/settings/normal?forceUpdate=false\" \\\n  | jq '.basestationStatus | {wifiStatus, wifiRssi, wallPower, backupBattery,\n                              gsmStatus, gsmRssi, rfJamming, cellCarrier}'\n```\n\n### PINs — cleartext, ask first\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/settings/normal?forceUpdate=false\" | jq '.settings.pins'\n```\n\nReturns the master and duress codes and every named user PIN in the clear. Only\nrun it if the user actually wants the codes; `.settings.normal` above is the\nsafe projection for everything else.\n\n## Writes\n\nConfirm with the user first — these move real hardware.\n\n```bash\n# Arm / disarm (no request body)\nss_api POST \"/ss3/subscriptions/$SID/state/away\"\nss_api POST \"/ss3/subscriptions/$SID/state/home\"\nss_api POST \"/ss3/subscriptions/$SID/state/off\"\n\n# Locks (serial from the roster above)\nss_api POST \"/doorlock/$SID/<serial>/state\" '{\"state\":\"lock\"}'\nss_api POST \"/doorlock/$SID/<serial>/state\" '{\"state\":\"unlock\"}'\n```\n\n### Verify, don't assume\n\n```bash\narm_and_verify() {\n  local want=\"$1\"\n  ss_api POST \"/ss3/subscriptions/$SID/state/$want\" >/dev/null || return 1\n  sleep 3\n  local got\n  got=$(ss_api GET \"/users/$UID_/subscriptions?activeOnly=true\" \\\n        | jq -r '.subscriptions[0].location.system.alarmState')\n  case \"$got\" in\n    \"$(echo \"$want\" | tr '[:lower:]' '[:upper:]')\")       echo \"confirmed: $got\" ;;\n    \"$(echo \"$want\" | tr '[:lower:]' '[:upper:]')_COUNT\") echo \"in progress (exit delay): $got\" ;;\n    *) echo \"UNCONFIRMED: wanted $want, system reports $got\" >&2; return 1 ;;\n  esac\n}\n```\n\nThe `_COUNT` case is the exit delay counting down — a success, not a failure.\nComparing whole objects instead of this one field would report success\nunconditionally, because the timestamps advance on their own.\n\nFile v1.3.0:skill-card.md\n\n## Description:\n\nHelps an agent check SimpliSafe alarm, sensor, lock, event, and system status and, with authorization, control alarms and locks using shell commands.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nSimpliSafe account holders and authorized operators can ask an agent to check alarm state, sensor and lock status, and recent events, or explicitly request arming, disarming, locking, or unlocking.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Authenticated commands can disarm the alarm or unlock physical doors without an enforced confirmation gate.\n\nMitigation: Require explicit user confirmation before physical control commands; consider a separate read-only helper for routine checks.\n\nRisk: A long-lived refresh token grants the agent access to the SimpliSafe account.\n\nMitigation: Protect the refresh token, limit agent access, and revoke it if exposed.\n\nRisk: Settings responses may expose cleartext alarm PINs.\n\nMitigation: Request PIN data only when necessary and exclude the PIN block from ordinary settings queries.\n\nRisk: A successful command response may not mean an alarm or lock changed state.\n\nMitigation: Re-read the relevant state, allowing for arming delays and fresh lock status updates.\n\n## Reference(s):\n\n- [ClawHub skill release](https://clawhub.ai/chrischall/skills/simplisafe-mcp)\n- [SimpliSafe curl and jq recipes](references/recipes.md)\n- [SimpliSafe shell helpers](references/ss-helpers.sh)\n\n## Skill Output:\n\n**Output Type(s):** [Text, Shell commands, Configuration guidance]\n\n**Output Format:** [Markdown with shell examples and command output]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Live state may lag; verify physical control changes with a fresh status read.]\n\n## Skill Version(s):\n\n1.3.0 (source: server-resolved ClawHub release)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v1.2.4: 5 files, 9788 bytes\n\nFiles: references/recipes.md (6629b), references/ss-helpers.sh (5486b), skill-card.md (2255b), SKILL.md (6268b), _meta.json (133b)\n\nFile v1.2.4:SKILL.md\n\n---\nname: simplisafe-api\ndescription: Query and control a SimpliSafe alarm system from the shell with curl — read system state, sensors, locks, events and settings, and arm/disarm or lock/unlock. Use when the user asks about their SimpliSafe alarm, house sensors, door locks, or whether the system is armed. Requires a one-time browser login to mint a refresh token.\n---\n\n# SimpliSafe from the shell\n\nSimpliSafe's API is reachable **server-side** — no browser bridge, no extension,\nno signed-in tab. Auth is a bearer token minted from a long-lived OAuth refresh\ntoken, so everything here is plain `curl` + `jq`.\n\nRequires `curl` and `jq`.\n\n## One-time setup\n\nSimpliSafe issues no API keys. The credential is an OAuth refresh token, minted\nby a browser login you do **once**:\n\n```bash\nnode ~/git/simplisafe-mcp/scripts/bootstrap-auth.mjs           # prints an authorize URL\n# sign in, then copy the com.simplisafe.mobile://... URL the browser fails to open\nnode ~/git/simplisafe-mcp/scripts/bootstrap-auth.mjs \"<that URL>\"\n```\n\nThat writes `SIMPLISAFE_REFRESH_TOKEN` to `~/git/simplisafe-mcp/.env` (mode 0600).\nSimpliSafe does **not** rotate refresh tokens, so this survives indefinitely —\nuntil you sign out of all devices in the SimpliSafe app, which revokes it.\n\nTo capture the code from the browser: open DevTools → Network → tick **Preserve\nlog** *before* signing in, then find the failed navigation to\n`com.simplisafe.mobile://…?code=…` and copy its link address. The code is\nsingle-use and expires in ~2 minutes.\n\n## Core pattern\n\n```bash\nsource ~/git/simplisafe-mcp/skills/simplisafe-api/references/ss-helpers.sh\n\nss_api GET /api/authCheck | jq            # who am I\nSID=$(ss_sid)                             # the single active system id\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" | jq\n```\n\n`ss_api <METHOD> <PATH> [JSON_BODY]` attaches auth, prints the body on stdout,\nand on a non-2xx prints to **stderr** and returns 1 — so a failure never looks\nlike an empty result. Access tokens are cached in `$TMPDIR` (0600) and re-minted\nonly when stale; the helpers never write to the repo's `.env`.\n\n## Resolve first\n\nAlmost every path needs a **system id (`sid`)**, and getting one takes two calls\n(`authCheck` → `userId`, then the subscriptions route). `ss_sid` does both, and\n**fails loudly if the account has more than one system** rather than guessing —\npass the sid explicitly in that case.\n\nNote two routing traps:\n\n- The system version that decides `ss3/` routing is at\n  `location.system.version`, **not** the top-level `systemVersion` (a different\n  number entirely).\n- **Events and doorlock control are NOT under the `ss3/` prefix.** Everything\n  else is.\n\n## Reads\n\n```bash\n# Is it armed? -> OFF | HOME | AWAY | HOME_COUNT | AWAY_COUNT | ALARM\nss_api GET \"/users/$(ss_api GET /api/authCheck | jq -r .userId)/subscriptions?activeOnly=true\" \\\n  | jq -r '.subscriptions[0].location.system.alarmState'\n\n# Anything wrong with a sensor?\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq '[.sensors[] | select(.flags.offline or .flags.lowBattery) | {name, type, flags}]'\n\n# Recent activity\nss_api GET \"/subscriptions/$SID/events?numEvents=10\" \\\n  | jq -r '.events[] | \"\\(.eventTimestamp|todate)  \\(.info)\"'\n```\n\nMore recipes, including lock state and base-station health:\n[references/recipes.md](references/recipes.md).\n\n## Writes — read this before running one\n\nThese commands act on a **physical security system**. Disarming leaves a house\nunmonitored; arming can trip a siren and a monitoring-center dispatch; unlocking\nopens a real door. Confirm intent with the user before running any of them, and\nnever run one speculatively.\n\n```bash\nss_api POST \"/ss3/subscriptions/$SID/state/away\"      # or /home, /off — no body\nss_api POST \"/doorlock/$SID/<serial>/state\" '{\"state\":\"lock\"}'   # or \"unlock\"\n```\n\n**A 2xx is not proof it worked.** Re-read and check one field:\n\n```bash\nsleep 3\nss_api GET \"/users/$(ss_api GET /api/authCheck | jq -r .userId)/subscriptions?activeOnly=true\" \\\n  | jq -r '.subscriptions[0].location.system.alarmState'\n```\n\nFour things that make naive verification lie:\n\n- Arming reports `AWAY_COUNT` / `HOME_COUNT` while the exit delay runs, settling\n  to `AWAY` / `HOME`. That is success in progress, not failure.\n- Do **not** diff whole objects — `alarmStateTimestamp`, `stateUpdated` and\n  `lastUpdated` advance on their own, so any call would look successful.\n- **Re-read locks with `forceUpdate=true`.** The cached payload lags by minutes\n  and has been seen reporting a *jammed* lock as cleanly `unlocked`.\n- **A lock takes ~5 s to report**, so a 3-second check calls a successful unlock\n  a failure. Poll, don't sleep once.\n\n**Arming also LOCKS your doors.** Each lock's `setting` has `home`/`away` flags\nthat auto-lock on arm, and `homeToOff`/`awayToOff` controlling whether disarming\nunlocks them. With `homeToOff: 0` (common), arm-then-disarm leaves the doors\nlocked — so it is *not* a safe way to test arming. Check with:\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq -r '.sensors[] | select(.type==16) | \"\\(.name)\\t\\(.setting)\"'\n```\n\nA lock reporting `lockJamState: 1` will not respond to commands at all: the POST\nstill returns `200`, and nothing moves. That is a hardware fault, not a bad\nrequest.\n\n## PINs are cleartext\n\n`/ss3/subscriptions/$SID/settings/normal` returns `settings.pins` — the master,\nduress and named-user **alarm codes, in cleartext**, in the same payload as the\nharmless settings. Project `.settings.normal` unless the user explicitly wants\nthe codes:\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/settings/normal?forceUpdate=false\" | jq '.settings.normal'\n```\n\n## Notes\n\n- `forceUpdate=true` makes the base station re-poll its devices — slower and\n  harder on the hardware. Leave it `false` unless freshness matters.\n- Lock state encoding is counter-intuitive: `lockState` **1 = locked, 2 =\n  unlocked**, and `lockJamState` overrides both.\n- Device type ids `21`, `23` and `24` appear on real systems but aren't in any\n  public enum — handle unknown types gracefully.\n- Avoid tight polling; the API rate-limits.\n\nFull verified endpoint reference: `~/git/simplisafe-mcp/docs/SIMPLISAFE-API.md`.\n\nFile v1.2.4:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"simplisafe-mcp\",\n  \"version\": \"1.2.4\",\n  \"publishedAt\": 1791168714524\n}\n\nFile v1.2.4:references/recipes.md\n\n# SimpliSafe curl + jq recipes\n\nAll examples assume:\n\n```bash\nsource ~/git/simplisafe-mcp/skills/simplisafe-api/references/ss-helpers.sh\nSID=$(ss_sid)\nUID_=$(ss_api GET /api/authCheck | jq -r .userId)\n```\n\nEvery recipe below was run against a live account.\n\n## System\n\n### Full system state\n\n```bash\nss_api GET \"/users/$UID_/subscriptions?activeOnly=true\" \\\n  | jq '.subscriptions[] | {\n      sid,\n      name: .location.locationName,\n      state: .location.system.alarmState,\n      alarming: .location.system.isAlarming,\n      offline: .location.system.isOffline,\n      powerOutage: .location.system.powerOutage,\n      conn: .location.system.connType,\n      version: .location.system.version\n    }'\n```\n\n### One-line \"is the house armed?\"\n\n```bash\nss_api GET \"/users/$UID_/subscriptions?activeOnly=true\" \\\n  | jq -r '.subscriptions[0].location.system | \"\\(.alarmState)\\(if .isAlarming then \"  ** ALARMING **\" else \"\" end)\"'\n```\n\n### Base-station messages (firmware notices, faults)\n\n```bash\nss_api GET \"/users/$UID_/subscriptions?activeOnly=true\" \\\n  | jq -r '.subscriptions[0].location.system.messages[] | \"\\(.timestamp|todate)  \\(.text)\"'\n```\n\n## Sensors\n\n### All devices, compact\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq -r '.sensors[] | \"\\(.type)\\t\\(.name)\\tlowBat=\\(.flags.lowBattery)\\toffline=\\(.flags.offline)\"' \\\n  | column -t\n```\n\n### Only devices needing attention\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq '[.sensors[]\n         | select(.flags.offline or .flags.lowBattery or (.status.triggered // false))\n         | {name, type, offline: .flags.offline, lowBattery: .flags.lowBattery, triggered: .status.triggered}]'\n```\n\n### Currently-open entry sensors (type 5)\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=true\" \\\n  | jq -r '.sensors[] | select(.type==5 and .status.triggered==true) | .name'\n```\n\n`forceUpdate=true` is warranted here — an open/closed reading is only meaningful\nif it is fresh.\n\n### Device type ids\n\n| id | device | | id | device |\n| --- | --- | --- | --- | --- |\n| 0 | remote | | 13 | siren |\n| 1 | keypad | | 14 | smoke + CO |\n| 2 | keychain | | 15 | doorbell |\n| 3 | panic button | | 16 | **lock** |\n| 4 | motion | | 17 | outdoor camera |\n| 5 | **entry** | | 20 | motion v2 |\n| 6 | glass break | | 22 | outdoor bell box |\n| 7 | carbon monoxide | | 253 | lock keypad |\n| 8 | smoke | | | |\n| 9 | leak | | 21, 23, 24 | seen live, **not in any public enum** |\n| 10 | temperature | | | |\n| 12 | camera | | | |\n\nType 24 carries `smokeTriggered` / `coTriggered` / `tamper` / `endOfLife`.\n\n## Locks\n\n### Lock roster with decoded state\n\n`lockState` **1 = locked, 2 = unlocked**; `lockJamState` overrides both.\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq -r '.sensors[] | select(.type==16) |\n      \"\\(.name)\\t\\(if .status.lockJamState==1 then \"JAMMED\"\n                   elif .status.lockState==1 then \"locked\"\n                   else \"unlocked\" end)\\tlowBat=\\(.status.lockLowBattery)\\tpinPadOffline=\\(.status.pinPadOffline)\"' \\\n  | column -t\n```\n\n### Serials, for the write commands\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq -r '.sensors[] | select(.type==16) | \"\\(.name)\\t\\(.serial)\"'\n```\n\n## Events\n\n### Recent activity, human-readable\n\n```bash\nss_api GET \"/subscriptions/$SID/events?numEvents=20\" \\\n  | jq -r '.events[] | \"\\(.eventTimestamp|todate)  [\\(.eventType)]  \\(.info)  \\(.sensorName // \"\")\"'\n```\n\nNote: **no `ss3/` prefix** on this route, and `eventTimestamp` is in seconds.\n\n> **`numEvents` maxes out at 50.** Verified by bisection against the live API:\n> 50 succeeds, 51 and above return `400 InvalidParameter`. To reach further back,\n> page with `fromTimestamp` rather than asking for more at once.\n\n### Only alarms\n\n```bash\nss_api GET \"/subscriptions/$SID/events?numEvents=50\" \\\n  | jq -r '.events[] | select(.eventType==\"alarm\") | \"\\(.eventTimestamp|todate)  \\(.info)\"'\n```\n\n### Since a point in time\n\n```bash\nSINCE=$(date -v-24H +%s)     # macOS; GNU: date -d '24 hours ago' +%s\nss_api GET \"/subscriptions/$SID/events?fromTimestamp=$SINCE&numEvents=50\" \\\n  | jq -r '.events[] | \"\\(.eventTimestamp|todate)  \\(.info)\"'\n```\n\n### Who disarmed it, and when\n\n```bash\nss_api GET \"/subscriptions/$SID/events?numEvents=50\" \\\n  | jq -r '.events[] | select(.info | test(\"Disarm\"; \"i\")) | \"\\(.eventTimestamp|todate)  \\(.info)  by \\(.pinName // \"unknown\")\"'\n```\n\n## Settings and health\n\n### Delays and volumes (PIN block excluded)\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/settings/normal?forceUpdate=false\" \\\n  | jq '.settings.normal | {entryDelayHome, entryDelayAway, exitDelayHome, exitDelayAway,\n                            alarmVolume, alarmDuration, doorChime, voicePrompts}'\n```\n\n### Base-station health\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/settings/normal?forceUpdate=false\" \\\n  | jq '.basestationStatus | {wifiStatus, wifiRssi, wallPower, backupBattery,\n                              gsmStatus, gsmRssi, rfJamming, cellCarrier}'\n```\n\n### PINs — cleartext, ask first\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/settings/normal?forceUpdate=false\" | jq '.settings.pins'\n```\n\nReturns the master and duress codes and every named user PIN in the clear. Only\nrun it if the user actually wants the codes; `.settings.normal` above is the\nsafe projection for everything else.\n\n## Writes\n\nConfirm with the user first — these move real hardware.\n\n```bash\n# Arm / disarm (no request body)\nss_api POST \"/ss3/subscriptions/$SID/state/away\"\nss_api POST \"/ss3/subscriptions/$SID/state/home\"\nss_api POST \"/ss3/subscriptions/$SID/state/off\"\n\n# Locks (serial from the roster above)\nss_api POST \"/doorlock/$SID/<serial>/state\" '{\"state\":\"lock\"}'\nss_api POST \"/doorlock/$SID/<serial>/state\" '{\"state\":\"unlock\"}'\n```\n\n### Verify, don't assume\n\n```bash\narm_and_verify() {\n  local want=\"$1\"\n  ss_api POST \"/ss3/subscriptions/$SID/state/$want\" >/dev/null || return 1\n  sleep 3\n  local got\n  got=$(ss_api GET \"/users/$UID_/subscriptions?activeOnly=true\" \\\n        | jq -r '.subscriptions[0].location.system.alarmState')\n  case \"$got\" in\n    \"$(echo \"$want\" | tr '[:lower:]' '[:upper:]')\")       echo \"confirmed: $got\" ;;\n    \"$(echo \"$want\" | tr '[:lower:]' '[:upper:]')_COUNT\") echo \"in progress (exit delay): $got\" ;;\n    *) echo \"UNCONFIRMED: wanted $want, system reports $got\" >&2; return 1 ;;\n  esac\n}\n```\n\nThe `_COUNT` case is the exit delay counting down — a success, not a failure.\nComparing whole objects instead of this one field would report success\nunconditionally, because the timestamps advance on their own.\n\nFile v1.2.4:skill-card.md\n\n## Description:\n\nHelps an agent check SimpliSafe alarm, sensor, lock, event, and settings data and, when requested, control alarm and door-lock state using shell commands.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nSimpliSafe account holders and their agents can inspect home security status, sensors, locks, and recent activity, or request alarm and door-lock changes. A one-time browser login provides the required refresh token.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Authenticated commands can arm or disarm the alarm and lock or unlock doors without an enforced confirmation step.\n\nMitigation: Require explicit human confirmation before every alarm or lock state change; do not execute such commands speculatively.\n\nRisk: Access to the SimpliSafe account uses a long-lived refresh token, and settings can expose alarm PINs in cleartext.\n\nMitigation: Protect and revoke the refresh token when needed, and require explicit human confirmation before reading PINs; return only requested settings.\n\nRisk: A successful command response or cached lock reading may not reflect the actual physical state.\n\nMitigation: Re-read the specific state after an action, poll locks for a fresh reading, and check for jams before reporting success.\n\n## Reference(s):\n\n- [SimpliSafe command recipes](references/recipes.md)\n- [SimpliSafe shell helpers](references/ss-helpers.sh)\n- [ClawHub skill release](https://clawhub.ai/chrischall/skills/simplisafe-mcp)\n\n## Skill Output:\n\n**Output Type(s):** [Text, Shell commands, Guidance]\n\n**Output Format:** [Markdown with shell commands and status summaries]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [May report account security status or propose actions affecting physical alarm and lock state.]\n\n## Skill Version(s):\n\n1.2.4 (source: ClawHub release metadata)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v1.2.3: 5 files, 9669 bytes\n\nFiles: references/recipes.md (6629b), references/ss-helpers.sh (5486b), skill-card.md (1981b), SKILL.md (6268b), _meta.json (133b)\n\nFile v1.2.3:SKILL.md\n\n---\nname: simplisafe-api\ndescription: Query and control a SimpliSafe alarm system from the shell with curl — read system state, sensors, locks, events and settings, and arm/disarm or lock/unlock. Use when the user asks about their SimpliSafe alarm, house sensors, door locks, or whether the system is armed. Requires a one-time browser login to mint a refresh token.\n---\n\n# SimpliSafe from the shell\n\nSimpliSafe's API is reachable **server-side** — no browser bridge, no extension,\nno signed-in tab. Auth is a bearer token minted from a long-lived OAuth refresh\ntoken, so everything here is plain `curl` + `jq`.\n\nRequires `curl` and `jq`.\n\n## One-time setup\n\nSimpliSafe issues no API keys. The credential is an OAuth refresh token, minted\nby a browser login you do **once**:\n\n```bash\nnode ~/git/simplisafe-mcp/scripts/bootstrap-auth.mjs           # prints an authorize URL\n# sign in, then copy the com.simplisafe.mobile://... URL the browser fails to open\nnode ~/git/simplisafe-mcp/scripts/bootstrap-auth.mjs \"<that URL>\"\n```\n\nThat writes `SIMPLISAFE_REFRESH_TOKEN` to `~/git/simplisafe-mcp/.env` (mode 0600).\nSimpliSafe does **not** rotate refresh tokens, so this survives indefinitely —\nuntil you sign out of all devices in the SimpliSafe app, which revokes it.\n\nTo capture the code from the browser: open DevTools → Network → tick **Preserve\nlog** *before* signing in, then find the failed navigation to\n`com.simplisafe.mobile://…?code=…` and copy its link address. The code is\nsingle-use and expires in ~2 minutes.\n\n## Core pattern\n\n```bash\nsource ~/git/simplisafe-mcp/skills/simplisafe-api/references/ss-helpers.sh\n\nss_api GET /api/authCheck | jq            # who am I\nSID=$(ss_sid)                             # the single active system id\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" | jq\n```\n\n`ss_api <METHOD> <PATH> [JSON_BODY]` attaches auth, prints the body on stdout,\nand on a non-2xx prints to **stderr** and returns 1 — so a failure never looks\nlike an empty result. Access tokens are cached in `$TMPDIR` (0600) and re-minted\nonly when stale; the helpers never write to the repo's `.env`.\n\n## Resolve first\n\nAlmost every path needs a **system id (`sid`)**, and getting one takes two calls\n(`authCheck` → `userId`, then the subscriptions route). `ss_sid` does both, and\n**fails loudly if the account has more than one system** rather than guessing —\npass the sid explicitly in that case.\n\nNote two routing traps:\n\n- The system version that decides `ss3/` routing is at\n  `location.system.version`, **not** the top-level `systemVersion` (a different\n  number entirely).\n- **Events and doorlock control are NOT under the `ss3/` prefix.** Everything\n  else is.\n\n## Reads\n\n```bash\n# Is it armed? -> OFF | HOME | AWAY | HOME_COUNT | AWAY_COUNT | ALARM\nss_api GET \"/users/$(ss_api GET /api/authCheck | jq -r .userId)/subscriptions?activeOnly=true\" \\\n  | jq -r '.subscriptions[0].location.system.alarmState'\n\n# Anything wrong with a sensor?\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq '[.sensors[] | select(.flags.offline or .flags.lowBattery) | {name, type, flags}]'\n\n# Recent activity\nss_api GET \"/subscriptions/$SID/events?numEvents=10\" \\\n  | jq -r '.events[] | \"\\(.eventTimestamp|todate)  \\(.info)\"'\n```\n\nMore recipes, including lock state and base-station health:\n[references/recipes.md](references/recipes.md).\n\n## Writes — read this before running one\n\nThese commands act on a **physical security system**. Disarming leaves a house\nunmonitored; arming can trip a siren and a monitoring-center dispatch; unlocking\nopens a real door. Confirm intent with the user before running any of them, and\nnever run one speculatively.\n\n```bash\nss_api POST \"/ss3/subscriptions/$SID/state/away\"      # or /home, /off — no body\nss_api POST \"/doorlock/$SID/<serial>/state\" '{\"state\":\"lock\"}'   # or \"unlock\"\n```\n\n**A 2xx is not proof it worked.** Re-read and check one field:\n\n```bash\nsleep 3\nss_api GET \"/users/$(ss_api GET /api/authCheck | jq -r .userId)/subscriptions?activeOnly=true\" \\\n  | jq -r '.subscriptions[0].location.system.alarmState'\n```\n\nFour things that make naive verification lie:\n\n- Arming reports `AWAY_COUNT` / `HOME_COUNT` while the exit delay runs, settling\n  to `AWAY` / `HOME`. That is success in progress, not failure.\n- Do **not** diff whole objects — `alarmStateTimestamp`, `stateUpdated` and\n  `lastUpdated` advance on their own, so any call would look successful.\n- **Re-read locks with `forceUpdate=true`.** The cached payload lags by minutes\n  and has been seen reporting a *jammed* lock as cleanly `unlocked`.\n- **A lock takes ~5 s to report**, so a 3-second check calls a successful unlock\n  a failure. Poll, don't sleep once.\n\n**Arming also LOCKS your doors.** Each lock's `setting` has `home`/`away` flags\nthat auto-lock on arm, and `homeToOff`/`awayToOff` controlling whether disarming\nunlocks them. With `homeToOff: 0` (common), arm-then-disarm leaves the doors\nlocked — so it is *not* a safe way to test arming. Check with:\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq -r '.sensors[] | select(.type==16) | \"\\(.name)\\t\\(.setting)\"'\n```\n\nA lock reporting `lockJamState: 1` will not respond to commands at all: the POST\nstill returns `200`, and nothing moves. That is a hardware fault, not a bad\nrequest.\n\n## PINs are cleartext\n\n`/ss3/subscriptions/$SID/settings/normal` returns `settings.pins` — the master,\nduress and named-user **alarm codes, in cleartext**, in the same payload as the\nharmless settings. Project `.settings.normal` unless the user explicitly wants\nthe codes:\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/settings/normal?forceUpdate=false\" | jq '.settings.normal'\n```\n\n## Notes\n\n- `forceUpdate=true` makes the base station re-poll its devices — slower and\n  harder on the hardware. Leave it `false` unless freshness matters.\n- Lock state encoding is counter-intuitive: `lockState` **1 = locked, 2 =\n  unlocked**, and `lockJamState` overrides both.\n- Device type ids `21`, `23` and `24` appear on real systems but aren't in any\n  public enum — handle unknown types gracefully.\n- Avoid tight polling; the API rate-limits.\n\nFull verified endpoint reference: `~/git/simplisafe-mcp/docs/SIMPLISAFE-API.md`.\n\nFile v1.2.3:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"simplisafe-mcp\",\n  \"version\": \"1.2.3\",\n  \"publishedAt\": 1790991824705\n}\n\nFile v1.2.3:references/recipes.md\n\n# SimpliSafe curl + jq recipes\n\nAll examples assume:\n\n```bash\nsource ~/git/simplisafe-mcp/skills/simplisafe-api/references/ss-helpers.sh\nSID=$(ss_sid)\nUID_=$(ss_api GET /api/authCheck | jq -r .userId)\n```\n\nEvery recipe below was run against a live account.\n\n## System\n\n### Full system state\n\n```bash\nss_api GET \"/users/$UID_/subscriptions?activeOnly=true\" \\\n  | jq '.subscriptions[] | {\n      sid,\n      name: .location.locationName,\n      state: .location.system.alarmState,\n      alarming: .location.system.isAlarming,\n      offline: .location.system.isOffline,\n      powerOutage: .location.system.powerOutage,\n      conn: .location.system.connType,\n      version: .location.system.version\n    }'\n```\n\n### One-line \"is the house armed?\"\n\n```bash\nss_api GET \"/users/$UID_/subscriptions?activeOnly=true\" \\\n  | jq -r '.subscriptions[0].location.system | \"\\(.alarmState)\\(if .isAlarming then \"  ** ALARMING **\" else \"\" end)\"'\n```\n\n### Base-station messages (firmware notices, faults)\n\n```bash\nss_api GET \"/users/$UID_/subscriptions?activeOnly=true\" \\\n  | jq -r '.subscriptions[0].location.system.messages[] | \"\\(.timestamp|todate)  \\(.text)\"'\n```\n\n## Sensors\n\n### All devices, compact\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq -r '.sensors[] | \"\\(.type)\\t\\(.name)\\tlowBat=\\(.flags.lowBattery)\\toffline=\\(.flags.offline)\"' \\\n  | column -t\n```\n\n### Only devices needing attention\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq '[.sensors[]\n         | select(.flags.offline or .flags.lowBattery or (.status.triggered // false))\n         | {name, type, offline: .flags.offline, lowBattery: .flags.lowBattery, triggered: .status.triggered}]'\n```\n\n### Currently-open entry sensors (type 5)\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=true\" \\\n  | jq -r '.sensors[] | select(.type==5 and .status.triggered==true) | .name'\n```\n\n`forceUpdate=true` is warranted here — an open/closed reading is only meaningful\nif it is fresh.\n\n### Device type ids\n\n| id | device | | id | device |\n| --- | --- | --- | --- | --- |\n| 0 | remote | | 13 | siren |\n| 1 | keypad | | 14 | smoke + CO |\n| 2 | keychain | | 15 | doorbell |\n| 3 | panic button | | 16 | **lock** |\n| 4 | motion | | 17 | outdoor camera |\n| 5 | **entry** | | 20 | motion v2 |\n| 6 | glass break | | 22 | outdoor bell box |\n| 7 | carbon monoxide | | 253 | lock keypad |\n| 8 | smoke | | | |\n| 9 | leak | | 21, 23, 24 | seen live, **not in any public enum** |\n| 10 | temperature | | | |\n| 12 | camera | | | |\n\nType 24 carries `smokeTriggered` / `coTriggered` / `tamper` / `endOfLife`.\n\n## Locks\n\n### Lock roster with decoded state\n\n`lockState` **1 = locked, 2 = unlocked**; `lockJamState` overrides both.\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq -r '.sensors[] | select(.type==16) |\n      \"\\(.name)\\t\\(if .status.lockJamState==1 then \"JAMMED\"\n                   elif .status.lockState==1 then \"locked\"\n                   else \"unlocked\" end)\\tlowBat=\\(.status.lockLowBattery)\\tpinPadOffline=\\(.status.pinPadOffline)\"' \\\n  | column -t\n```\n\n### Serials, for the write commands\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq -r '.sensors[] | select(.type==16) | \"\\(.name)\\t\\(.serial)\"'\n```\n\n## Events\n\n### Recent activity, human-readable\n\n```bash\nss_api GET \"/subscriptions/$SID/events?numEvents=20\" \\\n  | jq -r '.events[] | \"\\(.eventTimestamp|todate)  [\\(.eventType)]  \\(.info)  \\(.sensorName // \"\")\"'\n```\n\nNote: **no `ss3/` prefix** on this route, and `eventTimestamp` is in seconds.\n\n> **`numEvents` maxes out at 50.** Verified by bisection against the live API:\n> 50 succeeds, 51 and above return `400 InvalidParameter`. To reach further back,\n> page with `fromTimestamp` rather than asking for more at once.\n\n### Only alarms\n\n```bash\nss_api GET \"/subscriptions/$SID/events?numEvents=50\" \\\n  | jq -r '.events[] | select(.eventType==\"alarm\") | \"\\(.eventTimestamp|todate)  \\(.info)\"'\n```\n\n### Since a point in time\n\n```bash\nSINCE=$(date -v-24H +%s)     # macOS; GNU: date -d '24 hours ago' +%s\nss_api GET \"/subscriptions/$SID/events?fromTimestamp=$SINCE&numEvents=50\" \\\n  | jq -r '.events[] | \"\\(.eventTimestamp|todate)  \\(.info)\"'\n```\n\n### Who disarmed it, and when\n\n```bash\nss_api GET \"/subscriptions/$SID/events?numEvents=50\" \\\n  | jq -r '.events[] | select(.info | test(\"Disarm\"; \"i\")) | \"\\(.eventTimestamp|todate)  \\(.info)  by \\(.pinName // \"unknown\")\"'\n```\n\n## Settings and health\n\n### Delays and volumes (PIN block excluded)\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/settings/normal?forceUpdate=false\" \\\n  | jq '.settings.normal | {entryDelayHome, entryDelayAway, exitDelayHome, exitDelayAway,\n                            alarmVolume, alarmDuration, doorChime, voicePrompts}'\n```\n\n### Base-station health\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/settings/normal?forceUpdate=false\" \\\n  | jq '.basestationStatus | {wifiStatus, wifiRssi, wallPower, backupBattery,\n                              gsmStatus, gsmRssi, rfJamming, cellCarrier}'\n```\n\n### PINs — cleartext, ask first\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/settings/normal?forceUpdate=false\" | jq '.settings.pins'\n```\n\nReturns the master and duress codes and every named user PIN in the clear. Only\nrun it if the user actually wants the codes; `.settings.normal` above is the\nsafe projection for everything else.\n\n## Writes\n\nConfirm with the user first — these move real hardware.\n\n```bash\n# Arm / disarm (no request body)\nss_api POST \"/ss3/subscriptions/$SID/state/away\"\nss_api POST \"/ss3/subscriptions/$SID/state/home\"\nss_api POST \"/ss3/subscriptions/$SID/state/off\"\n\n# Locks (serial from the roster above)\nss_api POST \"/doorlock/$SID/<serial>/state\" '{\"state\":\"lock\"}'\nss_api POST \"/doorlock/$SID/<serial>/state\" '{\"state\":\"unlock\"}'\n```\n\n### Verify, don't assume\n\n```bash\narm_and_verify() {\n  local want=\"$1\"\n  ss_api POST \"/ss3/subscriptions/$SID/state/$want\" >/dev/null || return 1\n  sleep 3\n  local got\n  got=$(ss_api GET \"/users/$UID_/subscriptions?activeOnly=true\" \\\n        | jq -r '.subscriptions[0].location.system.alarmState')\n  case \"$got\" in\n    \"$(echo \"$want\" | tr '[:lower:]' '[:upper:]')\")       echo \"confirmed: $got\" ;;\n    \"$(echo \"$want\" | tr '[:lower:]' '[:upper:]')_COUNT\") echo \"in progress (exit delay): $got\" ;;\n    *) echo \"UNCONFIRMED: wanted $want, system reports $got\" >&2; return 1 ;;\n  esac\n}\n```\n\nThe `_COUNT` case is the exit delay counting down — a success, not a failure.\nComparing whole objects instead of this one field would report success\nunconditionally, because the timestamps advance on their own.\n\nFile v1.2.3:skill-card.md\n\n## Description:\n\nHelps an agent inspect and control a SimpliSafe alarm system, sensors, and door locks using authenticated shell commands.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nSimpliSafe users can ask an agent to check alarm status, sensors, locks, events, and settings, or to arm, disarm, lock, and unlock with their authorization.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Authenticated commands can disarm the alarm or unlock physical doors without an enforced confirmation gate.\n\nMitigation: Require explicit user confirmation for each arm, disarm, lock, or unlock command and verify the resulting state.\n\nRisk: The refresh token grants broad control over the security system.\n\nMitigation: Keep the token file private and revoke the token when the skill is no longer used.\n\nRisk: Settings responses can expose alarm PINs in cleartext.\n\nMitigation: Avoid requesting PINs unless necessary and return only the settings fields needed for the request.\n\n## Reference(s):\n\n- [ClawHub skill release](https://clawhub.ai/chrischall/skills/simplisafe-mcp)\n- [SimpliSafe command recipes](references/recipes.md)\n- [SimpliSafe shell helpers](references/ss-helpers.sh)\n\n## Skill Output:\n\n**Output Type(s):** [Shell commands, Guidance]\n\n**Output Format:** [Markdown with shell commands and explanations]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Commands require authenticated SimpliSafe access; physical security changes require explicit user confirmation.]\n\n## Skill Version(s):\n\n1.2.3 (source: ClawHub release metadata)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v1.2.2: 5 files, 9705 bytes\n\nFiles: references/recipes.md (6629b), references/ss-helpers.sh (5486b), skill-card.md (1935b), SKILL.md (6268b), _meta.json (133b)\n\nFile v1.2.2:SKILL.md\n\n---\nname: simplisafe-api\ndescription: Query and control a SimpliSafe alarm system from the shell with curl — read system state, sensors, locks, events and settings, and arm/disarm or lock/unlock. Use when the user asks about their SimpliSafe alarm, house sensors, door locks, or whether the system is armed. Requires a one-time browser login to mint a refresh token.\n---\n\n# SimpliSafe from the shell\n\nSimpliSafe's API is reachable **server-side** — no browser bridge, no extension,\nno signed-in tab. Auth is a bearer token minted from a long-lived OAuth refresh\ntoken, so everything here is plain `curl` + `jq`.\n\nRequires `curl` and `jq`.\n\n## One-time setup\n\nSimpliSafe issues no API keys. The credential is an OAuth refresh token, minted\nby a browser login you do **once**:\n\n```bash\nnode ~/git/simplisafe-mcp/scripts/bootstrap-auth.mjs           # prints an authorize URL\n# sign in, then copy the com.simplisafe.mobile://... URL the browser fails to open\nnode ~/git/simplisafe-mcp/scripts/bootstrap-auth.mjs \"<that URL>\"\n```\n\nThat writes `SIMPLISAFE_REFRESH_TOKEN` to `~/git/simplisafe-mcp/.env` (mode 0600).\nSimpliSafe does **not** rotate refresh tokens, so this survives indefinitely —\nuntil you sign out of all devices in the SimpliSafe app, which revokes it.\n\nTo capture the code from the browser: open DevTools → Network → tick **Preserve\nlog** *before* signing in, then find the failed navigation to\n`com.simplisafe.mobile://…?code=…` and copy its link address. The code is\nsingle-use and expires in ~2 minutes.\n\n## Core pattern\n\n```bash\nsource ~/git/simplisafe-mcp/skills/simplisafe-api/references/ss-helpers.sh\n\nss_api GET /api/authCheck | jq            # who am I\nSID=$(ss_sid)                             # the single active system id\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" | jq\n```\n\n`ss_api <METHOD> <PATH> [JSON_BODY]` attaches auth, prints the body on stdout,\nand on a non-2xx prints to **stderr** and returns 1 — so a failure never looks\nlike an empty result. Access tokens are cached in `$TMPDIR` (0600) and re-minted\nonly when stale; the helpers never write to the repo's `.env`.\n\n## Resolve first\n\nAlmost every path needs a **system id (`sid`)**, and getting one takes two calls\n(`authCheck` → `userId`, then the subscriptions route). `ss_sid` does both, and\n**fails loudly if the account has more than one system** rather than guessing —\npass the sid explicitly in that case.\n\nNote two routing traps:\n\n- The system version that decides `ss3/` routing is at\n  `location.system.version`, **not** the top-level `systemVersion` (a different\n  number entirely).\n- **Events and doorlock control are NOT under the `ss3/` prefix.** Everything\n  else is.\n\n## Reads\n\n```bash\n# Is it armed? -> OFF | HOME | AWAY | HOME_COUNT | AWAY_COUNT | ALARM\nss_api GET \"/users/$(ss_api GET /api/authCheck | jq -r .userId)/subscriptions?activeOnly=true\" \\\n  | jq -r '.subscriptions[0].location.system.alarmState'\n\n# Anything wrong with a sensor?\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq '[.sensors[] | select(.flags.offline or .flags.lowBattery) | {name, type, flags}]'\n\n# Recent activity\nss_api GET \"/subscriptions/$SID/events?numEvents=10\" \\\n  | jq -r '.events[] | \"\\(.eventTimestamp|todate)  \\(.info)\"'\n```\n\nMore recipes, including lock state and base-station health:\n[references/recipes.md](references/recipes.md).\n\n## Writes — read this before running one\n\nThese commands act on a **physical security system**. Disarming leaves a house\nunmonitored; arming can trip a siren and a monitoring-center dispatch; unlocking\nopens a real door. Confirm intent with the user before running any of them, and\nnever run one speculatively.\n\n```bash\nss_api POST \"/ss3/subscriptions/$SID/state/away\"      # or /home, /off — no body\nss_api POST \"/doorlock/$SID/<serial>/state\" '{\"state\":\"lock\"}'   # or \"unlock\"\n```\n\n**A 2xx is not proof it worked.** Re-read and check one field:\n\n```bash\nsleep 3\nss_api GET \"/users/$(ss_api GET /api/authCheck | jq -r .userId)/subscriptions?activeOnly=true\" \\\n  | jq -r '.subscriptions[0].location.system.alarmState'\n```\n\nFour things that make naive verification lie:\n\n- Arming reports `AWAY_COUNT` / `HOME_COUNT` while the exit delay runs, settling\n  to `AWAY` / `HOME`. That is success in progress, not failure.\n- Do **not** diff whole objects — `alarmStateTimestamp`, `stateUpdated` and\n  `lastUpdated` advance on their own, so any call would look successful.\n- **Re-read locks with `forceUpdate=true`.** The cached payload lags by minutes\n  and has been seen reporting a *jammed* lock as cleanly `unlocked`.\n- **A lock takes ~5 s to report**, so a 3-second check calls a successful unlock\n  a failure. Poll, don't sleep once.\n\n**Arming also LOCKS your doors.** Each lock's `setting` has `home`/`away` flags\nthat auto-lock on arm, and `homeToOff`/`awayToOff` controlling whether disarming\nunlocks them. With `homeToOff: 0` (common), arm-then-disarm leaves the doors\nlocked — so it is *not* a safe way to test arming. Check with:\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq -r '.sensors[] | select(.type==16) | \"\\(.name)\\t\\(.setting)\"'\n```\n\nA lock reporting `lockJamState: 1` will not respond to commands at all: the POST\nstill returns `200`, and nothing moves. That is a hardware fault, not a bad\nrequest.\n\n## PINs are cleartext\n\n`/ss3/subscriptions/$SID/settings/normal` returns `settings.pins` — the master,\nduress and named-user **alarm codes, in cleartext**, in the same payload as the\nharmless settings. Project `.settings.normal` unless the user explicitly wants\nthe codes:\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/settings/normal?forceUpdate=false\" | jq '.settings.normal'\n```\n\n## Notes\n\n- `forceUpdate=true` makes the base station re-poll its devices — slower and\n  harder on the hardware. Leave it `false` unless freshness matters.\n- Lock state encoding is counter-intuitive: `lockState` **1 = locked, 2 =\n  unlocked**, and `lockJamState` overrides both.\n- Device type ids `21`, `23` and `24` appear on real systems but aren't in any\n  public enum — handle unknown types gracefully.\n- Avoid tight polling; the API rate-limits.\n\nFull verified endpoint reference: `~/git/simplisafe-mcp/docs/SIMPLISAFE-API.md`.\n\nFile v1.2.2:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"simplisafe-mcp\",\n  \"version\": \"1.2.2\",\n  \"publishedAt\": 1790787372655\n}\n\nFile v1.2.2:references/recipes.md\n\n# SimpliSafe curl + jq recipes\n\nAll examples assume:\n\n```bash\nsource ~/git/simplisafe-mcp/skills/simplisafe-api/references/ss-helpers.sh\nSID=$(ss_sid)\nUID_=$(ss_api GET /api/authCheck | jq -r .userId)\n```\n\nEvery recipe below was run against a live account.\n\n## System\n\n### Full system state\n\n```bash\nss_api GET \"/users/$UID_/subscriptions?activeOnly=true\" \\\n  | jq '.subscriptions[] | {\n      sid,\n      name: .location.locationName,\n      state: .location.system.alarmState,\n      alarming: .location.system.isAlarming,\n      offline: .location.system.isOffline,\n      powerOutage: .location.system.powerOutage,\n      conn: .location.system.connType,\n      version: .location.system.version\n    }'\n```\n\n### One-line \"is the house armed?\"\n\n```bash\nss_api GET \"/users/$UID_/subscriptions?activeOnly=true\" \\\n  | jq -r '.subscriptions[0].location.system | \"\\(.alarmState)\\(if .isAlarming then \"  ** ALARMING **\" else \"\" end)\"'\n```\n\n### Base-station messages (firmware notices, faults)\n\n```bash\nss_api GET \"/users/$UID_/subscriptions?activeOnly=true\" \\\n  | jq -r '.subscriptions[0].location.system.messages[] | \"\\(.timestamp|todate)  \\(.text)\"'\n```\n\n## Sensors\n\n### All devices, compact\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq -r '.sensors[] | \"\\(.type)\\t\\(.name)\\tlowBat=\\(.flags.lowBattery)\\toffline=\\(.flags.offline)\"' \\\n  | column -t\n```\n\n### Only devices needing attention\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq '[.sensors[]\n         | select(.flags.offline or .flags.lowBattery or (.status.triggered // false))\n         | {name, type, offline: .flags.offline, lowBattery: .flags.lowBattery, triggered: .status.triggered}]'\n```\n\n### Currently-open entry sensors (type 5)\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=true\" \\\n  | jq -r '.sensors[] | select(.type==5 and .status.triggered==true) | .name'\n```\n\n`forceUpdate=true` is warranted here — an open/closed reading is only meaningful\nif it is fresh.\n\n### Device type ids\n\n| id | device | | id | device |\n| --- | --- | --- | --- | --- |\n| 0 | remote | | 13 | siren |\n| 1 | keypad | | 14 | smoke + CO |\n| 2 | keychain | | 15 | doorbell |\n| 3 | panic button | | 16 | **lock** |\n| 4 | motion | | 17 | outdoor camera |\n| 5 | **entry** | | 20 | motion v2 |\n| 6 | glass break | | 22 | outdoor bell box |\n| 7 | carbon monoxide | | 253 | lock keypad |\n| 8 | smoke | | | |\n| 9 | leak | | 21, 23, 24 | seen live, **not in any public enum** |\n| 10 | temperature | | | |\n| 12 | camera | | | |\n\nType 24 carries `smokeTriggered` / `coTriggered` / `tamper` / `endOfLife`.\n\n## Locks\n\n### Lock roster with decoded state\n\n`lockState` **1 = locked, 2 = unlocked**; `lockJamState` overrides both.\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq -r '.sensors[] | select(.type==16) |\n      \"\\(.name)\\t\\(if .status.lockJamState==1 then \"JAMMED\"\n                   elif .status.lockState==1 then \"locked\"\n                   else \"unlocked\" end)\\tlowBat=\\(.status.lockLowBattery)\\tpinPadOffline=\\(.status.pinPadOffline)\"' \\\n  | column -t\n```\n\n### Serials, for the write commands\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq -r '.sensors[] | select(.type==16) | \"\\(.name)\\t\\(.serial)\"'\n```\n\n## Events\n\n### Recent activity, human-readable\n\n```bash\nss_api GET \"/subscriptions/$SID/events?numEvents=20\" \\\n  | jq -r '.events[] | \"\\(.eventTimestamp|todate)  [\\(.eventType)]  \\(.info)  \\(.sensorName // \"\")\"'\n```\n\nNote: **no `ss3/` prefix** on this route, and `eventTimestamp` is in seconds.\n\n> **`numEvents` maxes out at 50.** Verified by bisection against the live API:\n> 50 succeeds, 51 and above return `400 InvalidParameter`. To reach further back,\n> page with `fromTimestamp` rather than asking for more at once.\n\n### Only alarms\n\n```bash\nss_api GET \"/subscriptions/$SID/events?numEvents=50\" \\\n  | jq -r '.events[] | select(.eventType==\"alarm\") | \"\\(.eventTimestamp|todate)  \\(.info)\"'\n```\n\n### Since a point in time\n\n```bash\nSINCE=$(date -v-24H +%s)     # macOS; GNU: date -d '24 hours ago' +%s\nss_api GET \"/subscriptions/$SID/events?fromTimestamp=$SINCE&numEvents=50\" \\\n  | jq -r '.events[] | \"\\(.eventTimestamp|todate)  \\(.info)\"'\n```\n\n### Who disarmed it, and when\n\n```bash\nss_api GET \"/subscriptions/$SID/events?numEvents=50\" \\\n  | jq -r '.events[] | select(.info | test(\"Disarm\"; \"i\")) | \"\\(.eventTimestamp|todate)  \\(.info)  by \\(.pinName // \"unknown\")\"'\n```\n\n## Settings and health\n\n### Delays and volumes (PIN block excluded)\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/settings/normal?forceUpdate=false\" \\\n  | jq '.settings.normal | {entryDelayHome, entryDelayAway, exitDelayHome, exitDelayAway,\n                            alarmVolume, alarmDuration, doorChime, voicePrompts}'\n```\n\n### Base-station health\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/settings/normal?forceUpdate=false\" \\\n  | jq '.basestationStatus | {wifiStatus, wifiRssi, wallPower, backupBattery,\n                              gsmStatus, gsmRssi, rfJamming, cellCarrier}'\n```\n\n### PINs — cleartext, ask first\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/settings/normal?forceUpdate=false\" | jq '.settings.pins'\n```\n\nReturns the master and duress codes and every named user PIN in the clear. Only\nrun it if the user actually wants the codes; `.settings.normal` above is the\nsafe projection for everything else.\n\n## Writes\n\nConfirm with the user first — these move real hardware.\n\n```bash\n# Arm / disarm (no request body)\nss_api POST \"/ss3/subscriptions/$SID/state/away\"\nss_api POST \"/ss3/subscriptions/$SID/state/home\"\nss_api POST \"/ss3/subscriptions/$SID/state/off\"\n\n# Locks (serial from the roster above)\nss_api POST \"/doorlock/$SID/<serial>/state\" '{\"state\":\"lock\"}'\nss_api POST \"/doorlock/$SID/<serial>/state\" '{\"state\":\"unlock\"}'\n```\n\n### Verify, don't assume\n\n```bash\narm_and_verify() {\n  local want=\"$1\"\n  ss_api POST \"/ss3/subscriptions/$SID/state/$want\" >/dev/null || return 1\n  sleep 3\n  local got\n  got=$(ss_api GET \"/users/$UID_/subscriptions?activeOnly=true\" \\\n        | jq -r '.subscriptions[0].location.system.alarmState')\n  case \"$got\" in\n    \"$(echo \"$want\" | tr '[:lower:]' '[:upper:]')\")       echo \"confirmed: $got\" ;;\n    \"$(echo \"$want\" | tr '[:lower:]' '[:upper:]')_COUNT\") echo \"in progress (exit delay): $got\" ;;\n    *) echo \"UNCONFIRMED: wanted $want, system reports $got\" >&2; return 1 ;;\n  esac\n}\n```\n\nThe `_COUNT` case is the exit delay counting down — a success, not a failure.\nComparing whole objects instead of this one field would report success\nunconditionally, because the timestamps advance on their own.\n\nFile v1.2.2:skill-card.md\n\n## Description:\n\nHelps an agent check SimpliSafe alarm, sensor, lock, event, and settings information and, with user confirmation, arm or disarm the system or lock or unlock doors.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nSimpliSafe users can ask an agent to check home security status, sensors, locks, and recent activity, or to control the alarm and locks after explicitly confirming the action.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: An agent can disarm the alarm or unlock a real door, potentially leaving a home unprotected.\n\nMitigation: Require explicit user confirmation for every arm, disarm, lock, or unlock action; check the resulting device state.\n\nRisk: The skill can expose cleartext alarm PINs and long-lived credentials, and its token cache may be accessible on a shared machine.\n\nMitigation: Avoid retrieving or displaying PINs unless specifically requested; use a single-user machine and a private TMPDIR or corrected token-cache location.\n\n## Reference(s):\n\n- [SimpliSafe shell recipes](references/recipes.md)\n- [ClawHub skill release](https://clawhub.ai/chrischall/skills/simplisafe-mcp)\n\n## Skill Output:\n\n**Output Type(s):** [Text, Shell commands, Guidance]\n\n**Output Format:** [Markdown with shell commands]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Can report security-system status and propose or run user-confirmed control commands; responses may contain sensitive device information.]\n\n## Skill Version(s):\n\n1.2.2 (source: ClawHub release)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v1.2.1: 5 files, 9738 bytes\n\nFiles: references/recipes.md (6629b), references/ss-helpers.sh (5486b), skill-card.md (2027b), SKILL.md (6268b), _meta.json (133b)\n\nFile v1.2.1:SKILL.md\n\n---\nname: simplisafe-api\ndescription: Query and control a SimpliSafe alarm system from the shell with curl — read system state, sensors, locks, events and settings, and arm/disarm or lock/unlock. Use when the user asks about their SimpliSafe alarm, house sensors, door locks, or whether the system is armed. Requires a one-time browser login to mint a refresh token.\n---\n\n# SimpliSafe from the shell\n\nSimpliSafe's API is reachable **server-side** — no browser bridge, no extension,\nno signed-in tab. Auth is a bearer token minted from a long-lived OAuth refresh\ntoken, so everything here is plain `curl` + `jq`.\n\nRequires `curl` and `jq`.\n\n## One-time setup\n\nSimpliSafe issues no API keys. The credential is an OAuth refresh token, minted\nby a browser login you do **once**:\n\n```bash\nnode ~/git/simplisafe-mcp/scripts/bootstrap-auth.mjs           # prints an authorize URL\n# sign in, then copy the com.simplisafe.mobile://... URL the browser fails to open\nnode ~/git/simplisafe-mcp/scripts/bootstrap-auth.mjs \"<that URL>\"\n```\n\nThat writes `SIMPLISAFE_REFRESH_TOKEN` to `~/git/simplisafe-mcp/.env` (mode 0600).\nSimpliSafe does **not** rotate refresh tokens, so this survives indefinitely —\nuntil you sign out of all devices in the SimpliSafe app, which revokes it.\n\nTo capture the code from the browser: open DevTools → Network → tick **Preserve\nlog** *before* signing in, then find the failed navigation to\n`com.simplisafe.mobile://…?code=…` and copy its link address. The code is\nsingle-use and expires in ~2 minutes.\n\n## Core pattern\n\n```bash\nsource ~/git/simplisafe-mcp/skills/simplisafe-api/references/ss-helpers.sh\n\nss_api GET /api/authCheck | jq            # who am I\nSID=$(ss_sid)                             # the single active system id\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" | jq\n```\n\n`ss_api <METHOD> <PATH> [JSON_BODY]` attaches auth, prints the body on stdout,\nand on a non-2xx prints to **stderr** and returns 1 — so a failure never looks\nlike an empty result. Access tokens are cached in `$TMPDIR` (0600) and re-minted\nonly when stale; the helpers never write to the repo's `.env`.\n\n## Resolve first\n\nAlmost every path needs a **system id (`sid`)**, and getting one takes two calls\n(`authCheck` → `userId`, then the subscriptions route). `ss_sid` does both, and\n**fails loudly if the account has more than one system** rather than guessing —\npass the sid explicitly in that case.\n\nNote two routing traps:\n\n- The system version that decides `ss3/` routing is at\n  `location.system.version`, **not** the top-level `systemVersion` (a different\n  number entirely).\n- **Events and doorlock control are NOT under the `ss3/` prefix.** Everything\n  else is.\n\n## Reads\n\n```bash\n# Is it armed? -> OFF | HOME | AWAY | HOME_COUNT | AWAY_COUNT | ALARM\nss_api GET \"/users/$(ss_api GET /api/authCheck | jq -r .userId)/subscriptions?activeOnly=true\" \\\n  | jq -r '.subscriptions[0].location.system.alarmState'\n\n# Anything wrong with a sensor?\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq '[.sensors[] | select(.flags.offline or .flags.lowBattery) | {name, type, flags}]'\n\n# Recent activity\nss_api GET \"/subscriptions/$SID/events?numEvents=10\" \\\n  | jq -r '.events[] | \"\\(.eventTimestamp|todate)  \\(.info)\"'\n```\n\nMore recipes, including lock state and base-station health:\n[references/recipes.md](references/recipes.md).\n\n## Writes — read this before running one\n\nThese commands act on a **physical security system**. Disarming leaves a house\nunmonitored; arming can trip a siren and a monitoring-center dispatch; unlocking\nopens a real door. Confirm intent with the user before running any of them, and\nnever run one speculatively.\n\n```bash\nss_api POST \"/ss3/subscriptions/$SID/state/away\"      # or /home, /off — no body\nss_api POST \"/doorlock/$SID/<serial>/state\" '{\"state\":\"lock\"}'   # or \"unlock\"\n```\n\n**A 2xx is not proof it worked.** Re-read and check one field:\n\n```bash\nsleep 3\nss_api GET \"/users/$(ss_api GET /api/authCheck | jq -r .userId)/subscriptions?activeOnly=true\" \\\n  | jq -r '.subscriptions[0].location.system.alarmState'\n```\n\nFour things that make naive verification lie:\n\n- Arming reports `AWAY_COUNT` / `HOME_COUNT` while the exit delay runs, settling\n  to `AWAY` / `HOME`. That is success in progress, not failure.\n- Do **not** diff whole objects — `alarmStateTimestamp`, `stateUpdated` and\n  `lastUpdated` advance on their own, so any call would look successful.\n- **Re-read locks with `forceUpdate=true`.** The cached payload lags by minutes\n  and has been seen reporting a *jammed* lock as cleanly `unlocked`.\n- **A lock takes ~5 s to report**, so a 3-second check calls a successful unlock\n  a failure. Poll, don't sleep once.\n\n**Arming also LOCKS your doors.** Each lock's `setting` has `home`/`away` flags\nthat auto-lock on arm, and `homeToOff`/`awayToOff` controlling whether disarming\nunlocks them. With `homeToOff: 0` (common), arm-then-disarm leaves the doors\nlocked — so it is *not* a safe way to test arming. Check with:\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq -r '.sensors[] | select(.type==16) | \"\\(.name)\\t\\(.setting)\"'\n```\n\nA lock reporting `lockJamState: 1` will not respond to commands at all: the POST\nstill returns `200`, and nothing moves. That is a hardware fault, not a bad\nrequest.\n\n## PINs are cleartext\n\n`/ss3/subscriptions/$SID/settings/normal` returns `settings.pins` — the master,\nduress and named-user **alarm codes, in cleartext**, in the same payload as the\nharmless settings. Project `.settings.normal` unless the user explicitly wants\nthe codes:\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/settings/normal?forceUpdate=false\" | jq '.settings.normal'\n```\n\n## Notes\n\n- `forceUpdate=true` makes the base station re-poll its devices — slower and\n  harder on the hardware. Leave it `false` unless freshness matters.\n- Lock state encoding is counter-intuitive: `lockState` **1 = locked, 2 =\n  unlocked**, and `lockJamState` overrides both.\n- Device type ids `21`, `23` and `24` appear on real systems but aren't in any\n  public enum — handle unknown types gracefully.\n- Avoid tight polling; the API rate-limits.\n\nFull verified endpoint reference: `~/git/simplisafe-mcp/docs/SIMPLISAFE-API.md`.\n\nFile v1.2.1:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"simplisafe-mcp\",\n  \"version\": \"1.2.1\",\n  \"publishedAt\": 1790351721906\n}\n\nFile v1.2.1:references/recipes.md\n\n# SimpliSafe curl + jq recipes\n\nAll examples assume:\n\n```bash\nsource ~/git/simplisafe-mcp/skills/simplisafe-api/references/ss-helpers.sh\nSID=$(ss_sid)\nUID_=$(ss_api GET /api/authCheck | jq -r .userId)\n```\n\nEvery recipe below was run against a live account.\n\n## System\n\n### Full system state\n\n```bash\nss_api GET \"/users/$UID_/subscriptions?activeOnly=true\" \\\n  | jq '.subscriptions[] | {\n      sid,\n      name: .location.locationName,\n      state: .location.system.alarmState,\n      alarming: .location.system.isAlarming,\n      offline: .location.system.isOffline,\n      powerOutage: .location.system.powerOutage,\n      conn: .location.system.connType,\n      version: .location.system.version\n    }'\n```\n\n### One-line \"is the house armed?\"\n\n```bash\nss_api GET \"/users/$UID_/subscriptions?activeOnly=true\" \\\n  | jq -r '.subscriptions[0].location.system | \"\\(.alarmState)\\(if .isAlarming then \"  ** ALARMING **\" else \"\" end)\"'\n```\n\n### Base-station messages (firmware notices, faults)\n\n```bash\nss_api GET \"/users/$UID_/subscriptions?activeOnly=true\" \\\n  | jq -r '.subscriptions[0].location.system.messages[] | \"\\(.timestamp|todate)  \\(.text)\"'\n```\n\n## Sensors\n\n### All devices, compact\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq -r '.sensors[] | \"\\(.type)\\t\\(.name)\\tlowBat=\\(.flags.lowBattery)\\toffline=\\(.flags.offline)\"' \\\n  | column -t\n```\n\n### Only devices needing attention\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq '[.sensors[]\n         | select(.flags.offline or .flags.lowBattery or (.status.triggered // false))\n         | {name, type, offline: .flags.offline, lowBattery: .flags.lowBattery, triggered: .status.triggered}]'\n```\n\n### Currently-open entry sensors (type 5)\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=true\" \\\n  | jq -r '.sensors[] | select(.type==5 and .status.triggered==true) | .name'\n```\n\n`forceUpdate=true` is warranted here — an open/closed reading is only meaningful\nif it is fresh.\n\n### Device type ids\n\n| id | device | | id | device |\n| --- | --- | --- | --- | --- |\n| 0 | remote | | 13 | siren |\n| 1 | keypad | | 14 | smoke + CO |\n| 2 | keychain | | 15 | doorbell |\n| 3 | panic button | | 16 | **lock** |\n| 4 | motion | | 17 | outdoor camera |\n| 5 | **entry** | | 20 | motion v2 |\n| 6 | glass break | | 22 | outdoor bell box |\n| 7 | carbon monoxide | | 253 | lock keypad |\n| 8 | smoke | | | |\n| 9 | leak | | 21, 23, 24 | seen live, **not in any public enum** |\n| 10 | temperature | | | |\n| 12 | camera | | | |\n\nType 24 carries `smokeTriggered` / `coTriggered` / `tamper` / `endOfLife`.\n\n## Locks\n\n### Lock roster with decoded state\n\n`lockState` **1 = locked, 2 = unlocked**; `lockJamState` overrides both.\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq -r '.sensors[] | select(.type==16) |\n      \"\\(.name)\\t\\(if .status.lockJamState==1 then \"JAMMED\"\n                   elif .status.lockState==1 then \"locked\"\n                   else \"unlocked\" end)\\tlowBat=\\(.status.lockLowBattery)\\tpinPadOffline=\\(.status.pinPadOffline)\"' \\\n  | column -t\n```\n\n### Serials, for the write commands\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq -r '.sensors[] | select(.type==16) | \"\\(.name)\\t\\(.serial)\"'\n```\n\n## Events\n\n### Recent activity, human-readable\n\n```bash\nss_api GET \"/subscriptions/$SID/events?numEvents=20\" \\\n  | jq -r '.events[] | \"\\(.eventTimestamp|todate)  [\\(.eventType)]  \\(.info)  \\(.sensorName // \"\")\"'\n```\n\nNote: **no `ss3/` prefix** on this route, and `eventTimestamp` is in seconds.\n\n> **`numEvents` maxes out at 50.** Verified by bisection against the live API:\n> 50 succeeds, 51 and above return `400 InvalidParameter`. To reach further back,\n> page with `fromTimestamp` rather than asking for more at once.\n\n### Only alarms\n\n```bash\nss_api GET \"/subscriptions/$SID/events?numEvents=50\" \\\n  | jq -r '.events[] | select(.eventType==\"alarm\") | \"\\(.eventTimestamp|todate)  \\(.info)\"'\n```\n\n### Since a point in time\n\n```bash\nSINCE=$(date -v-24H +%s)     # macOS; GNU: date -d '24 hours ago' +%s\nss_api GET \"/subscriptions/$SID/events?fromTimestamp=$SINCE&numEvents=50\" \\\n  | jq -r '.events[] | \"\\(.eventTimestamp|todate)  \\(.info)\"'\n```\n\n### Who disarmed it, and when\n\n```bash\nss_api GET \"/subscriptions/$SID/events?numEvents=50\" \\\n  | jq -r '.events[] | select(.info | test(\"Disarm\"; \"i\")) | \"\\(.eventTimestamp|todate)  \\(.info)  by \\(.pinName // \"unknown\")\"'\n```\n\n## Settings and health\n\n### Delays and volumes (PIN block excluded)\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/settings/normal?forceUpdate=false\" \\\n  | jq '.settings.normal | {entryDelayHome, entryDelayAway, exitDelayHome, exitDelayAway,\n                            alarmVolume, alarmDuration, doorChime, voicePrompts}'\n```\n\n### Base-station health\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/settings/normal?forceUpdate=false\" \\\n  | jq '.basestationStatus | {wifiStatus, wifiRssi, wallPower, backupBattery,\n                              gsmStatus, gsmRssi, rfJamming, cellCarrier}'\n```\n\n### PINs — cleartext, ask first\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/settings/normal?forceUpdate=false\" | jq '.settings.pins'\n```\n\nReturns the master and duress codes and every named user PIN in the clear. Only\nrun it if the user actually wants the codes; `.settings.normal` above is the\nsafe projection for everything else.\n\n## Writes\n\nConfirm with the user first — these move real hardware.\n\n```bash\n# Arm / disarm (no request body)\nss_api POST \"/ss3/subscriptions/$SID/state/away\"\nss_api POST \"/ss3/subscriptions/$SID/state/home\"\nss_api POST \"/ss3/subscriptions/$SID/state/off\"\n\n# Locks (serial from the roster above)\nss_api POST \"/doorlock/$SID/<serial>/state\" '{\"state\":\"lock\"}'\nss_api POST \"/doorlock/$SID/<serial>/state\" '{\"state\":\"unlock\"}'\n```\n\n### Verify, don't assume\n\n```bash\narm_and_verify() {\n  local want=\"$1\"\n  ss_api POST \"/ss3/subscriptions/$SID/state/$want\" >/dev/null || return 1\n  sleep 3\n  local got\n  got=$(ss_api GET \"/users/$UID_/subscriptions?activeOnly=true\" \\\n        | jq -r '.subscriptions[0].location.system.alarmState')\n  case \"$got\" in\n    \"$(echo \"$want\" | tr '[:lower:]' '[:upper:]')\")       echo \"confirmed: $got\" ;;\n    \"$(echo \"$want\" | tr '[:lower:]' '[:upper:]')_COUNT\") echo \"in progress (exit delay): $got\" ;;\n    *) echo \"UNCONFIRMED: wanted $want, system reports $got\" >&2; return 1 ;;\n  esac\n}\n```\n\nThe `_COUNT` case is the exit delay counting down — a success, not a failure.\nComparing whole objects instead of this one field would report success\nunconditionally, because the timestamps advance on their own.\n\nFile v1.2.1:skill-card.md\n\n## Description:\n\nProvides shell-based guidance to check SimpliSafe alarms, sensors, locks, events, and settings, and to arm, disarm, lock, or unlock devices.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nSimpliSafe account holders and their agents use this skill to inspect system status and device activity or, with explicit approval, change alarm and lock states.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Stored credentials give the agent access to the SimpliSafe account.\n\nMitigation: Use a private environment, keep the refresh token out of shared workspaces, and set TMPDIR to a private 0700 directory.\n\nRisk: Alarm and lock commands can change physical security without an enforced confirmation gate.\n\nMitigation: Require explicit user confirmation before arming, disarming, locking, or unlocking, and verify the resulting device state.\n\nRisk: Settings routes may expose alarm PINs in cleartext.\n\nMitigation: Avoid PIN-retrieval routes unless specifically needed and limit settings output to the requested fields.\n\n## Reference(s):\n\n- [ClawHub skill release](https://clawhub.ai/chrischall/skills/simplisafe-mcp)\n- [SimpliSafe command recipes](references/recipes.md)\n- [SimpliSafe shell helpers](references/ss-helpers.sh)\n\n## Skill Output:\n\n**Output Type(s):** [Guidance, Shell commands, Text]\n\n**Output Format:** [Markdown with shell commands and human-readable API results]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Requires a SimpliSafe account, a browser-issued refresh token, curl, and jq.]\n\n## Skill Version(s):\n\n1.2.1 (source: ClawHub release evidence)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v1.2.0: 5 files, 9849 bytes\n\nFiles: references/recipes.md (6629b), references/ss-helpers.sh (5486b), skill-card.md (2353b), SKILL.md (6268b), _meta.json (133b)\n\nFile v1.2.0:SKILL.md\n\n---\nname: simplisafe-api\ndescription: Query and control a SimpliSafe alarm system from the shell with curl — read system state, sensors, locks, events and settings, and arm/disarm or lock/unlock. Use when the user asks about their SimpliSafe alarm, house sensors, door locks, or whether the system is armed. Requires a one-time browser login to mint a refresh token.\n---\n\n# SimpliSafe from the shell\n\nSimpliSafe's API is reachable **server-side** — no browser bridge, no extension,\nno signed-in tab. Auth is a bearer token minted from a long-lived OAuth refresh\ntoken, so everything here is plain `curl` + `jq`.\n\nRequires `curl` and `jq`.\n\n## One-time setup\n\nSimpliSafe issues no API keys. The credential is an OAuth refresh token, minted\nby a browser login you do **once**:\n\n```bash\nnode ~/git/simplisafe-mcp/scripts/bootstrap-auth.mjs           # prints an authorize URL\n# sign in, then copy the com.simplisafe.mobile://... URL the browser fails to open\nnode ~/git/simplisafe-mcp/scripts/bootstrap-auth.mjs \"<that URL>\"\n```\n\nThat writes `SIMPLISAFE_REFRESH_TOKEN` to `~/git/simplisafe-mcp/.env` (mode 0600).\nSimpliSafe does **not** rotate refresh tokens, so this survives indefinitely —\nuntil you sign out of all devices in the SimpliSafe app, which revokes it.\n\nTo capture the code from the browser: open DevTools → Network → tick **Preserve\nlog** *before* signing in, then find the failed navigation to\n`com.simplisafe.mobile://…?code=…` and copy its link address. The code is\nsingle-use and expires in ~2 minutes.\n\n## Core pattern\n\n```bash\nsource ~/git/simplisafe-mcp/skills/simplisafe-api/references/ss-helpers.sh\n\nss_api GET /api/authCheck | jq            # who am I\nSID=$(ss_sid)                             # the single active system id\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" | jq\n```\n\n`ss_api <METHOD> <PATH> [JSON_BODY]` attaches auth, prints the body on stdout,\nand on a non-2xx prints to **stderr** and returns 1 — so a failure never looks\nlike an empty result. Access tokens are cached in `$TMPDIR` (0600) and re-minted\nonly when stale; the helpers never write to the repo's `.env`.\n\n## Resolve first\n\nAlmost every path needs a **system id (`sid`)**, and getting one takes two calls\n(`authCheck` → `userId`, then the subscriptions route). `ss_sid` does both, and\n**fails loudly if the account has more than one system** rather than guessing —\npass the sid explicitly in that case.\n\nNote two routing traps:\n\n- The system version that decides `ss3/` routing is at\n  `location.system.version`, **not** the top-level `systemVersion` (a different\n  number entirely).\n- **Events and doorlock control are NOT under the `ss3/` prefix.** Everything\n  else is.\n\n## Reads\n\n```bash\n# Is it armed? -> OFF | HOME | AWAY | HOME_COUNT | AWAY_COUNT | ALARM\nss_api GET \"/users/$(ss_api GET /api/authCheck | jq -r .userId)/subscriptions?activeOnly=true\" \\\n  | jq -r '.subscriptions[0].location.system.alarmState'\n\n# Anything wrong with a sensor?\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq '[.sensors[] | select(.flags.offline or .flags.lowBattery) | {name, type, flags}]'\n\n# Recent activity\nss_api GET \"/subscriptions/$SID/events?numEvents=10\" \\\n  | jq -r '.events[] | \"\\(.eventTimestamp|todate)  \\(.info)\"'\n```\n\nMore recipes, including lock state and base-station health:\n[references/recipes.md](references/recipes.md).\n\n## Writes — read this before running one\n\nThese commands act on a **physical security system**. Disarming leaves a house\nunmonitored; arming can trip a siren and a monitoring-center dispatch; unlocking\nopens a real door. Confirm intent with the user before running any of them, and\nnever run one speculatively.\n\n```bash\nss_api POST \"/ss3/subscriptions/$SID/state/away\"      # or /home, /off — no body\nss_api POST \"/doorlock/$SID/<serial>/state\" '{\"state\":\"lock\"}'   # or \"unlock\"\n```\n\n**A 2xx is not proof it worked.** Re-read and check one field:\n\n```bash\nsleep 3\nss_api GET \"/users/$(ss_api GET /api/authCheck | jq -r .userId)/subscriptions?activeOnly=true\" \\\n  | jq -r '.subscriptions[0].location.system.alarmState'\n```\n\nFour things that make naive verification lie:\n\n- Arming reports `AWAY_COUNT` / `HOME_COUNT` while the exit delay runs, settling\n  to `AWAY` / `HOME`. That is success in progress, not failure.\n- Do **not** diff whole objects — `alarmStateTimestamp`, `stateUpdated` and\n  `lastUpdated` advance on their own, so any call would look successful.\n- **Re-read locks with `forceUpdate=true`.** The cached payload lags by minutes\n  and has been seen reporting a *jammed* lock as cleanly `unlocked`.\n- **A lock takes ~5 s to report**, so a 3-second check calls a successful unlock\n  a failure. Poll, don't sleep once.\n\n**Arming also LOCKS your doors.** Each lock's `setting` has `home`/`away` flags\nthat auto-lock on arm, and `homeToOff`/`awayToOff` controlling whether disarming\nunlocks them. With `homeToOff: 0` (common), arm-then-disarm leaves the doors\nlocked — so it is *not* a safe way to test arming. Check with:\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq -r '.sensors[] | select(.type==16) | \"\\(.name)\\t\\(.setting)\"'\n```\n\nA lock reporting `lockJamState: 1` will not respond to commands at all: the POST\nstill returns `200`, and nothing moves. That is a hardware fault, not a bad\nrequest.\n\n## PINs are cleartext\n\n`/ss3/subscriptions/$SID/settings/normal` returns `settings.pins` — the master,\nduress and named-user **alarm codes, in cleartext**, in the same payload as the\nharmless settings. Project `.settings.normal` unless the user explicitly wants\nthe codes:\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/settings/normal?forceUpdate=false\" | jq '.settings.normal'\n```\n\n## Notes\n\n- `forceUpdate=true` makes the base station re-poll its devices — slower and\n  harder on the hardware. Leave it `false` unless freshness matters.\n- Lock state encoding is counter-intuitive: `lockState` **1 = locked, 2 =\n  unlocked**, and `lockJamState` overrides both.\n- Device type ids `21`, `23` and `24` appear on real systems but aren't in any\n  public enum — handle unknown types gracefully.\n- Avoid tight polling; the API rate-limits.\n\nFull verified endpoint reference: `~/git/simplisafe-mcp/docs/SIMPLISAFE-API.md`.\n\nFile v1.2.0:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"simplisafe-mcp\",\n  \"version\": \"1.2.0\",\n  \"publishedAt\": 1790262750934\n}\n\nFile v1.2.0:references/recipes.md\n\n# SimpliSafe curl + jq recipes\n\nAll examples assume:\n\n```bash\nsource ~/git/simplisafe-mcp/skills/simplisafe-api/references/ss-helpers.sh\nSID=$(ss_sid)\nUID_=$(ss_api GET /api/authCheck | jq -r .userId)\n```\n\nEvery recipe below was run against a live account.\n\n## System\n\n### Full system state\n\n```bash\nss_api GET \"/users/$UID_/subscriptions?activeOnly=true\" \\\n  | jq '.subscriptions[] | {\n      sid,\n      name: .location.locationName,\n      state: .location.system.alarmState,\n      alarming: .location.system.isAlarming,\n      offline: .location.system.isOffline,\n      powerOutage: .location.system.powerOutage,\n      conn: .location.system.connType,\n      version: .location.system.version\n    }'\n```\n\n### One-line \"is the house armed?\"\n\n```bash\nss_api GET \"/users/$UID_/subscriptions?activeOnly=true\" \\\n  | jq -r '.subscriptions[0].location.system | \"\\(.alarmState)\\(if .isAlarming then \"  ** ALARMING **\" else \"\" end)\"'\n```\n\n### Base-station messages (firmware notices, faults)\n\n```bash\nss_api GET \"/users/$UID_/subscriptions?activeOnly=true\" \\\n  | jq -r '.subscriptions[0].location.system.messages[] | \"\\(.timestamp|todate)  \\(.text)\"'\n```\n\n## Sensors\n\n### All devices, compact\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq -r '.sensors[] | \"\\(.type)\\t\\(.name)\\tlowBat=\\(.flags.lowBattery)\\toffline=\\(.flags.offline)\"' \\\n  | column -t\n```\n\n### Only devices needing attention\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq '[.sensors[]\n         | select(.flags.offline or .flags.lowBattery or (.status.triggered // false))\n         | {name, type, offline: .flags.offline, lowBattery: .flags.lowBattery, triggered: .status.triggered}]'\n```\n\n### Currently-open entry sensors (type 5)\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=true\" \\\n  | jq -r '.sensors[] | select(.type==5 and .status.triggered==true) | .name'\n```\n\n`forceUpdate=true` is warranted here — an open/closed reading is only meaningful\nif it is fresh.\n\n### Device type ids\n\n| id | device | | id | device |\n| --- | --- | --- | --- | --- |\n| 0 | remote | | 13 | siren |\n| 1 | keypad | | 14 | smoke + CO |\n| 2 | keychain | | 15 | doorbell |\n| 3 | panic button | | 16 | **lock** |\n| 4 | motion | | 17 | outdoor camera |\n| 5 | **entry** | | 20 | motion v2 |\n| 6 | glass break | | 22 | outdoor bell box |\n| 7 | carbon monoxide | | 253 | lock keypad |\n| 8 | smoke | | | |\n| 9 | leak | | 21, 23, 24 | seen live, **not in any public enum** |\n| 10 | temperature | | | |\n| 12 | camera | | | |\n\nType 24 carries `smokeTriggered` / `coTriggered` / `tamper` / `endOfLife`.\n\n## Locks\n\n### Lock roster with decoded state\n\n`lockState` **1 = locked, 2 = unlocked**; `lockJamState` overrides both.\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq -r '.sensors[] | select(.type==16) |\n      \"\\(.name)\\t\\(if .status.lockJamState==1 then \"JAMMED\"\n                   elif .status.lockState==1 then \"locked\"\n                   else \"unlocked\" end)\\tlowBat=\\(.status.lockLowBattery)\\tpinPadOffline=\\(.status.pinPadOffline)\"' \\\n  | column -t\n```\n\n### Serials, for the write commands\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq -r '.sensors[] | select(.type==16) | \"\\(.name)\\t\\(.serial)\"'\n```\n\n## Events\n\n### Recent activity, human-readable\n\n```bash\nss_api GET \"/subscriptions/$SID/events?numEvents=20\" \\\n  | jq -r '.events[] | \"\\(.eventTimestamp|todate)  [\\(.eventType)]  \\(.info)  \\(.sensorName // \"\")\"'\n```\n\nNote: **no `ss3/` prefix** on this route, and `eventTimestamp` is in seconds.\n\n> **`numEvents` maxes out at 50.** Verified by bisection against the live API:\n> 50 succeeds, 51 and above return `400 InvalidParameter`. To reach further back,\n> page with `fromTimestamp` rather than asking for more at once.\n\n### Only alarms\n\n```bash\nss_api GET \"/subscriptions/$SID/events?numEvents=50\" \\\n  | jq -r '.events[] | select(.eventType==\"alarm\") | \"\\(.eventTimestamp|todate)  \\(.info)\"'\n```\n\n### Since a point in time\n\n```bash\nSINCE=$(date -v-24H +%s)     # macOS; GNU: date -d '24 hours ago' +%s\nss_api GET \"/subscriptions/$SID/events?fromTimestamp=$SINCE&numEvents=50\" \\\n  | jq -r '.events[] | \"\\(.eventTimestamp|todate)  \\(.info)\"'\n```\n\n### Who disarmed it, and when\n\n```bash\nss_api GET \"/subscriptions/$SID/events?numEvents=50\" \\\n  | jq -r '.events[] | select(.info | test(\"Disarm\"; \"i\")) | \"\\(.eventTimestamp|todate)  \\(.info)  by \\(.pinName // \"unknown\")\"'\n```\n\n## Settings and health\n\n### Delays and volumes (PIN block excluded)\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/settings/normal?forceUpdate=false\" \\\n  | jq '.settings.normal | {entryDelayHome, entryDelayAway, exitDelayHome, exitDelayAway,\n                            alarmVolume, alarmDuration, doorChime, voicePrompts}'\n```\n\n### Base-station health\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/settings/normal?forceUpdate=false\" \\\n  | jq '.basestationStatus | {wifiStatus, wifiRssi, wallPower, backupBattery,\n                              gsmStatus, gsmRssi, rfJamming, cellCarrier}'\n```\n\n### PINs — cleartext, ask first\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/settings/normal?forceUpdate=false\" | jq '.settings.pins'\n```\n\nReturns the master and duress codes and every named user PIN in the clear. Only\nrun it if the user actually wants the codes; `.settings.normal` above is the\nsafe projection for everything else.\n\n## Writes\n\nConfirm with the user first — these move real hardware.\n\n```bash\n# Arm / disarm (no request body)\nss_api POST \"/ss3/subscriptions/$SID/state/away\"\nss_api POST \"/ss3/subscriptions/$SID/state/home\"\nss_api POST \"/ss3/subscriptions/$SID/state/off\"\n\n# Locks (serial from the roster above)\nss_api POST \"/doorlock/$SID/<serial>/state\" '{\"state\":\"lock\"}'\nss_api POST \"/doorlock/$SID/<serial>/state\" '{\"state\":\"unlock\"}'\n```\n\n### Verify, don't assume\n\n```bash\narm_and_verify() {\n  local want=\"$1\"\n  ss_api POST \"/ss3/subscriptions/$SID/state/$want\" >/dev/null || return 1\n  sleep 3\n  local got\n  got=$(ss_api GET \"/users/$UID_/subscriptions?activeOnly=true\" \\\n        | jq -r '.subscriptions[0].location.system.alarmState')\n  case \"$got\" in\n    \"$(echo \"$want\" | tr '[:lower:]' '[:upper:]')\")       echo \"confirmed: $got\" ;;\n    \"$(echo \"$want\" | tr '[:lower:]' '[:upper:]')_COUNT\") echo \"in progress (exit delay): $got\" ;;\n    *) echo \"UNCONFIRMED: wanted $want, system reports $got\" >&2; return 1 ;;\n  esac\n}\n```\n\nThe `_COUNT` case is the exit delay counting down — a success, not a failure.\nComparing whole objects instead of this one field would report success\nunconditionally, because the timestamps advance on their own.\n\nFile v1.2.0:skill-card.md\n\n## Description:\n\nQuery and control a SimpliSafe alarm system from the shell with curl: read system state, sensors, locks, events, and settings, and perform requested arm, disarm, lock, or unlock actions after setup.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nExternal users and developers use this skill to inspect SimpliSafe alarm status, sensors, locks, events, and settings from shell workflows. It can also help perform explicitly requested arming, disarming, locking, and unlocking actions after confirming intent.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: The skill can perform live alarm and lock changes without an enforced confirmation gate.\n\nMitigation: Use read-only queries by default and require explicit user confirmation before arm, disarm, lock, or unlock commands.\n\nRisk: SimpliSafe refresh tokens and cached access tokens grant access to a live security system if exposed.\n\nMitigation: Protect SIMPLISAFE_REFRESH_TOKEN and token cache files, keep credential files private, and avoid shared or untrusted shell environments.\n\nRisk: A successful HTTP response may not prove that an alarm or lock state changed as intended.\n\nMitigation: Re-read the specific alarm or lock state after a change, request fresh lock state when needed, and treat unconfirmed results as failures.\n\n## Reference(s):\n\n- [ClawHub skill page](https://clawhub.ai/chrischall/skills/simplisafe-mcp)\n- [SimpliSafe curl + jq recipes](references/recipes.md)\n- [SimpliSafe shell helpers](references/ss-helpers.sh)\n\n## Skill Output:\n\n**Output Type(s):** [text, markdown, shell commands, configuration, guidance]\n\n**Output Format:** [Markdown with inline shell commands and JSON examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [May include live SimpliSafe API calls that require credentials and explicit user confirmation for control actions.]\n\n## Skill Version(s):\n\n1.2.0 (source: server release metadata)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v1.1.3: 5 files, 9705 bytes\n\nFiles: references/recipes.md (6629b), references/ss-helpers.sh (5486b), skill-card.md (2063b), SKILL.md (6268b), _meta.json (133b)\n\nFile v1.1.3:SKILL.md\n\n---\nname: simplisafe-api\ndescription: Query and control a SimpliSafe alarm system from the shell with curl — read system state, sensors, locks, events and settings, and arm/disarm or lock/unlock. Use when the user asks about their SimpliSafe alarm, house sensors, door locks, or whether the system is armed. Requires a one-time browser login to mint a refresh token.\n---\n\n# SimpliSafe from the shell\n\nSimpliSafe's API is reachable **server-side** — no browser bridge, no extension,\nno signed-in tab. Auth is a bearer token minted from a long-lived OAuth refresh\ntoken, so everything here is plain `curl` + `jq`.\n\nRequires `curl` and `jq`.\n\n## One-time setup\n\nSimpliSafe issues no API keys. The credential is an OAuth refresh token, minted\nby a browser login you do **once**:\n\n```bash\nnode ~/git/simplisafe-mcp/scripts/bootstrap-auth.mjs           # prints an authorize URL\n# sign in, then copy the com.simplisafe.mobile://... URL the browser fails to open\nnode ~/git/simplisafe-mcp/scripts/bootstrap-auth.mjs \"<that URL>\"\n```\n\nThat writes `SIMPLISAFE_REFRESH_TOKEN` to `~/git/simplisafe-mcp/.env` (mode 0600).\nSimpliSafe does **not** rotate refresh tokens, so this survives indefinitely —\nuntil you sign out of all devices in the SimpliSafe app, which revokes it.\n\nTo capture the code from the browser: open DevTools → Network → tick **Preserve\nlog** *before* signing in, then find the failed navigation to\n`com.simplisafe.mobile://…?code=…` and copy its link address. The code is\nsingle-use and expires in ~2 minutes.\n\n## Core pattern\n\n```bash\nsource ~/git/simplisafe-mcp/skills/simplisafe-api/references/ss-helpers.sh\n\nss_api GET /api/authCheck | jq            # who am I\nSID=$(ss_sid)                             # the single active system id\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" | jq\n```\n\n`ss_api <METHOD> <PATH> [JSON_BODY]` attaches auth, prints the body on stdout,\nand on a non-2xx prints to **stderr** and returns 1 — so a failure never looks\nlike an empty result. Access tokens are cached in `$TMPDIR` (0600) and re-minted\nonly when stale; the helpers never write to the repo's `.env`.\n\n## Resolve first\n\nAlmost every path needs a **system id (`sid`)**, and getting one takes two calls\n(`authCheck` → `userId`, then the subscriptions route). `ss_sid` does both, and\n**fails loudly if the account has more than one system** rather than guessing —\npass the sid explicitly in that case.\n\nNote two routing traps:\n\n- The system version that decides `ss3/` routing is at\n  `location.system.version`, **not** the top-level `systemVersion` (a different\n  number entirely).\n- **Events and doorlock control are NOT under the `ss3/` prefix.** Everything\n  else is.\n\n## Reads\n\n```bash\n# Is it armed? -> OFF | HOME | AWAY | HOME_COUNT | AWAY_COUNT | ALARM\nss_api GET \"/users/$(ss_api GET /api/authCheck | jq -r .userId)/subscriptions?activeOnly=true\" \\\n  | jq -r '.subscriptions[0].location.system.alarmState'\n\n# Anything wrong with a sensor?\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq '[.sensors[] | select(.flags.offline or .flags.lowBattery) | {name, type, flags}]'\n\n# Recent activity\nss_api GET \"/subscriptions/$SID/events?numEvents=10\" \\\n  | jq -r '.events[] | \"\\(.eventTimestamp|todate)  \\(.info)\"'\n```\n\nMore recipes, including lock state and base-station health:\n[references/recipes.md](references/recipes.md).\n\n## Writes — read this before running one\n\nThese commands act on a **physical security system**. Disarming leaves a house\nunmonitored; arming can trip a siren and a monitoring-center dispatch; unlocking\nopens a real door. Confirm intent with the user before running any of them, and\nnever run one speculatively.\n\n```bash\nss_api POST \"/ss3/subscriptions/$SID/state/away\"      # or /home, /off — no body\nss_api POST \"/doorlock/$SID/<serial>/state\" '{\"state\":\"lock\"}'   # or \"unlock\"\n```\n\n**A 2xx is not proof it worked.** Re-read and check one field:\n\n```bash\nsleep 3\nss_api GET \"/users/$(ss_api GET /api/authCheck | jq -r .userId)/subscriptions?activeOnly=true\" \\\n  | jq -r '.subscriptions[0].location.system.alarmState'\n```\n\nFour things that make naive verification lie:\n\n- Arming reports `AWAY_COUNT` / `HOME_COUNT` while the exit delay runs, settling\n  to `AWAY` / `HOME`. That is success in progress, not failure.\n- Do **not** diff whole objects — `alarmStateTimestamp`, `stateUpdated` and\n  `lastUpdated` advance on their own, so any call would look successful.\n- **Re-read locks with `forceUpdate=true`.** The cached payload lags by minutes\n  and has been seen reporting a *jammed* lock as cleanly `unlocked`.\n- **A lock takes ~5 s to report**, so a 3-second check calls a successful unlock\n  a failure. Poll, don't sleep once.\n\n**Arming also LOCKS your doors.** Each lock's `setting` has `home`/`away` flags\nthat auto-lock on arm, and `homeToOff`/`awayToOff` controlling whether disarming\nunlocks them. With `homeToOff: 0` (common), arm-then-disarm leaves the doors\nlocked — so it is *not* a safe way to test arming. Check with:\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq -r '.sensors[] | select(.type==16) | \"\\(.name)\\t\\(.setting)\"'\n```\n\nA lock reporting `lockJamState: 1` will not respond to commands at all: the POST\nstill returns `200`, and nothing moves. That is a hardware fault, not a bad\nrequest.\n\n## PINs are cleartext\n\n`/ss3/subscriptions/$SID/settings/normal` returns `settings.pins` — the master,\nduress and named-user **alarm codes, in cleartext**, in the same payload as the\nharmless settings. Project `.settings.normal` unless the user explicitly wants\nthe codes:\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/settings/normal?forceUpdate=false\" | jq '.settings.normal'\n```\n\n## Notes\n\n- `forceUpdate=true` makes the base station re-poll its devices — slower and\n  harder on the hardware. Leave it `false` unless freshness matters.\n- Lock state encoding is counter-intuitive: `lockState` **1 = locked, 2 =\n  unlocked**, and `lockJamState` overrides both.\n- Device type ids `21`, `23` and `24` appear on real systems but aren't in any\n  public enum — handle unknown types gracefully.\n- Avoid tight polling; the API rate-limits.\n\nFull verified endpoint reference: `~/git/simplisafe-mcp/docs/SIMPLISAFE-API.md`.\n\nFile v1.1.3:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"simplisafe-mcp\",\n  \"version\": \"1.1.3\",\n  \"publishedAt\": 1790199697440\n}\n\nFile v1.1.3:references/recipes.md\n\n# SimpliSafe curl + jq recipes\n\nAll examples assume:\n\n```bash\nsource ~/git/simplisafe-mcp/skills/simplisafe-api/references/ss-helpers.sh\nSID=$(ss_sid)\nUID_=$(ss_api GET /api/authCheck | jq -r .userId)\n```\n\nEvery recipe below was run against a live account.\n\n## System\n\n### Full system state\n\n```bash\nss_api GET \"/users/$UID_/subscriptions?activeOnly=true\" \\\n  | jq '.subscriptions[] | {\n      sid,\n      name: .location.locationName,\n      state: .location.system.alarmState,\n      alarming: .location.system.isAlarming,\n      offline: .location.system.isOffline,\n      powerOutage: .location.system.powerOutage,\n      conn: .location.system.connType,\n      version: .location.system.version\n    }'\n```\n\n### One-line \"is the house armed?\"\n\n```bash\nss_api GET \"/users/$UID_/subscriptions?activeOnly=true\" \\\n  | jq -r '.subscriptions[0].location.system | \"\\(.alarmState)\\(if .isAlarming then \"  ** ALARMING **\" else \"\" end)\"'\n```\n\n### Base-station messages (firmware notices, faults)\n\n```bash\nss_api GET \"/users/$UID_/subscriptions?activeOnly=true\" \\\n  | jq -r '.subscriptions[0].location.system.messages[] | \"\\(.timestamp|todate)  \\(.text)\"'\n```\n\n## Sensors\n\n### All devices, compact\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq -r '.sensors[] | \"\\(.type)\\t\\(.name)\\tlowBat=\\(.flags.lowBattery)\\toffline=\\(.flags.offline)\"' \\\n  | column -t\n```\n\n### Only devices needing attention\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq '[.sensors[]\n         | select(.flags.offline or .flags.lowBattery or (.status.triggered // false))\n         | {name, type, offline: .flags.offline, lowBattery: .flags.lowBattery, triggered: .status.triggered}]'\n```\n\n### Currently-open entry sensors (type 5)\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=true\" \\\n  | jq -r '.sensors[] | select(.type==5 and .status.triggered==true) | .name'\n```\n\n`forceUpdate=true` is warranted here — an open/closed reading is only meaningful\nif it is fresh.\n\n### Device type ids\n\n| id | device | | id | device |\n| --- | --- | --- | --- | --- |\n| 0 | remote | | 13 | siren |\n| 1 | keypad | | 14 | smoke + CO |\n| 2 | keychain | | 15 | doorbell |\n| 3 | panic button | | 16 | **lock** |\n| 4 | motion | | 17 | outdoor camera |\n| 5 | **entry** | | 20 | motion v2 |\n| 6 | glass break | | 22 | outdoor bell box |\n| 7 | carbon monoxide | | 253 | lock keypad |\n| 8 | smoke | | | |\n| 9 | leak | | 21, 23, 24 | seen live, **not in any public enum** |\n| 10 | temperature | | | |\n| 12 | camera | | | |\n\nType 24 carries `smokeTriggered` / `coTriggered` / `tamper` / `endOfLife`.\n\n## Locks\n\n### Lock roster with decoded state\n\n`lockState` **1 = locked, 2 = unlocked**; `lockJamState` overrides both.\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq -r '.sensors[] | select(.type==16) |\n      \"\\(.name)\\t\\(if .status.lockJamState==1 then \"JAMMED\"\n                   elif .status.lockState==1 then \"locked\"\n                   else \"unlocked\" end)\\tlowBat=\\(.status.lockLowBattery)\\tpinPadOffline=\\(.status.pinPadOffline)\"' \\\n  | column -t\n```\n\n### Serials, for the write commands\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq -r '.sensors[] | select(.type==16) | \"\\(.name)\\t\\(.serial)\"'\n```\n\n## Events\n\n### Recent activity, human-readable\n\n```bash\nss_api GET \"/subscriptions/$SID/events?numEvents=20\" \\\n  | jq -r '.events[] | \"\\(.eventTimestamp|todate)  [\\(.eventType)]  \\(.info)  \\(.sensorName // \"\")\"'\n```\n\nNote: **no `ss3/` prefix** on this route, and `eventTimestamp` is in seconds.\n\n> **`numEvents` maxes out at 50.** Verified by bisection against the live API:\n> 50 succeeds, 51 and above return `400 InvalidParameter`. To reach further back,\n> page with `fromTimestamp` rather than asking for more at once.\n\n### Only alarms\n\n```bash\nss_api GET \"/subscriptions/$SID/events?numEvents=50\" \\\n  | jq -r '.events[] | select(.eventType==\"alarm\") | \"\\(.eventTimestamp|todate)  \\(.info)\"'\n```\n\n### Since a point in time\n\n```bash\nSINCE=$(date -v-24H +%s)     # macOS; GNU: date -d '24 hours ago' +%s\nss_api GET \"/subscriptions/$SID/events?fromTimestamp=$SINCE&numEvents=50\" \\\n  | jq -r '.events[] | \"\\(.eventTimestamp|todate)  \\(.info)\"'\n```\n\n### Who disarmed it, and when\n\n```bash\nss_api GET \"/subscriptions/$SID/events?numEvents=50\" \\\n  | jq -r '.events[] | select(.info | test(\"Disarm\"; \"i\")) | \"\\(.eventTimestamp|todate)  \\(.info)  by \\(.pinName // \"unknown\")\"'\n```\n\n## Settings and health\n\n### Delays and volumes (PIN block excluded)\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/settings/normal?forceUpdate=false\" \\\n  | jq '.settings.normal | {entryDelayHome, entryDelayAway, exitDelayHome, exitDelayAway,\n                            alarmVolume, alarmDuration, doorChime, voicePrompts}'\n```\n\n### Base-station health\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/settings/normal?forceUpdate=false\" \\\n  | jq '.basestationStatus | {wifiStatus, wifiRssi, wallPower, backupBattery,\n                              gsmStatus, gsmRssi, rfJamming, cellCarrier}'\n```\n\n### PINs — cleartext, ask first\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/settings/normal?forceUpdate=false\" | jq '.settings.pins'\n```\n\nReturns the master and duress codes and every named user PIN in the clear. Only\nrun it if the user actually wants the codes; `.settings.normal` above is the\nsafe projection for everything else.\n\n## Writes\n\nConfirm with the user first — these move real hardware.\n\n```bash\n# Arm / disarm (no request body)\nss_api POST \"/ss3/subscriptions/$SID/state/away\"\nss_api POST \"/ss3/subscriptions/$SID/state/home\"\nss_api POST \"/ss3/subscriptions/$SID/state/off\"\n\n# Locks (serial from the roster above)\nss_api POST \"/doorlock/$SID/<serial>/state\" '{\"state\":\"lock\"}'\nss_api POST \"/doorlock/$SID/<serial>/state\" '{\"state\":\"unlock\"}'\n```\n\n### Verify, don't assume\n\n```bash\narm_and_verify() {\n  local want=\"$1\"\n  ss_api POST \"/ss3/subscriptions/$SID/state/$want\" >/dev/null || return 1\n  sleep 3\n  local got\n  got=$(ss_api GET \"/users/$UID_/subscriptions?activeOnly=true\" \\\n        | jq -r '.subscriptions[0].location.system.alarmState')\n  case \"$got\" in\n    \"$(echo \"$want\" | tr '[:lower:]' '[:upper:]')\")       echo \"confirmed: $got\" ;;\n    \"$(echo \"$want\" | tr '[:lower:]' '[:upper:]')_COUNT\") echo \"in progress (exit delay): $got\" ;;\n    *) echo \"UNCONFIRMED: wanted $want, system reports $got\" >&2; return 1 ;;\n  esac\n}\n```\n\nThe `_COUNT` case is the exit delay counting down — a success, not a failure.\nComparing whole objects instead of this one field would report success\nunconditionally, because the timestamps advance on their own.\n\nFile v1.1.3:skill-card.md\n\n## Description:\n\nQuery and control a SimpliSafe alarm system from the shell with curl, including system state, sensors, locks, events, settings, and arm, disarm, lock, or unlock actions.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nDevelopers and technically capable users use this skill to inspect and operate their own SimpliSafe system from an agent-assisted shell workflow, including authenticated status checks, event review, and explicit arm, disarm, lock, or unlock commands.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: The skill can control high-impact SimpliSafe alarm and lock actions, including arm, disarm, lock, unlock, and PIN-reading commands.\n\nMitigation: Require explicit user confirmation outside the helper before any arm, disarm, lock, unlock, or PIN-reading command.\n\nRisk: The security evidence reports weak executable safeguards and an unsafe token cache path.\n\nMitigation: Install only where authenticated SimpliSafe control is acceptable, prefer a single-user machine, and use a private TMPDIR or fix token cache handling before operation.\n\n## Reference(s):\n\n- [SimpliSafe curl + jq recipes](artifact/references/recipes.md)\n- [SimpliSafe shell helpers](artifact/references/ss-helpers.sh)\n\n## Skill Output:\n\n**Output Type(s):** [Text, Markdown, Shell commands, Configuration, Guidance]\n\n**Output Format:** [Markdown with inline shell commands and configuration guidance]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [May include authenticated API calls and explicit confirmation guidance for physical security actions.]\n\n## Skill Version(s):\n\n1.1.3 (source: server release evidence)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v1.1.2: 5 files, 10011 bytes\n\nFiles: references/recipes.md (6629b), references/ss-helpers.sh (5486b), skill-card.md (2634b), SKILL.md (6268b), _meta.json (133b)\n\nFile v1.1.2:SKILL.md\n\n---\nname: simplisafe-api\ndescription: Query and control a SimpliSafe alarm system from the shell with curl — read system state, sensors, locks, events and settings, and arm/disarm or lock/unlock. Use when the user asks about their SimpliSafe alarm, house sensors, door locks, or whether the system is armed. Requires a one-time browser login to mint a refresh token.\n---\n\n# SimpliSafe from the shell\n\nSimpliSafe's API is reachable **server-side** — no browser bridge, no extension,\nno signed-in tab. Auth is a bearer token minted from a long-lived OAuth refresh\ntoken, so everything here is plain `curl` + `jq`.\n\nRequires `curl` and `jq`.\n\n## One-time setup\n\nSimpliSafe issues no API keys. The credential is an OAuth refresh token, minted\nby a browser login you do **once**:\n\n```bash\nnode ~/git/simplisafe-mcp/scripts/bootstrap-auth.mjs           # prints an authorize URL\n# sign in, then copy the com.simplisafe.mobile://... URL the browser fails to open\nnode ~/git/simplisafe-mcp/scripts/bootstrap-auth.mjs \"<that URL>\"\n```\n\nThat writes `SIMPLISAFE_REFRESH_TOKEN` to `~/git/simplisafe-mcp/.env` (mode 0600).\nSimpliSafe does **not** rotate refresh tokens, so this survives indefinitely —\nuntil you sign out of all devices in the SimpliSafe app, which revokes it.\n\nTo capture the code from the browser: open DevTools → Network → tick **Preserve\nlog** *before* signing in, then find the failed navigation to\n`com.simplisafe.mobile://…?code=…` and copy its link address. The code is\nsingle-use and expires in ~2 minutes.\n\n## Core pattern\n\n```bash\nsource ~/git/simplisafe-mcp/skills/simplisafe-api/references/ss-helpers.sh\n\nss_api GET /api/authCheck | jq            # who am I\nSID=$(ss_sid)                             # the single active system id\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" | jq\n```\n\n`ss_api <METHOD> <PATH> [JSON_BODY]` attaches auth, prints the body on stdout,\nand on a non-2xx prints to **stderr** and returns 1 — so a failure never looks\nlike an empty result. Access tokens are cached in `$TMPDIR` (0600) and re-minted\nonly when stale; the helpers never write to the repo's `.env`.\n\n## Resolve first\n\nAlmost every path needs a **system id (`sid`)**, and getting one takes two calls\n(`authCheck` → `userId`, then the subscriptions route). `ss_sid` does both, and\n**fails loudly if the account has more than one system** rather than guessing —\npass the sid explicitly in that case.\n\nNote two routing traps:\n\n- The system version that decides `ss3/` routing is at\n  `location.system.version`, **not** the top-level `systemVersion` (a different\n  number entirely).\n- **Events and doorlock control are NOT under the `ss3/` prefix.** Everything\n  else is.\n\n## Reads\n\n```bash\n# Is it armed? -> OFF | HOME | AWAY | HOME_COUNT | AWAY_COUNT | ALARM\nss_api GET \"/users/$(ss_api GET /api/authCheck | jq -r .userId)/subscriptions?activeOnly=true\" \\\n  | jq -r '.subscriptions[0].location.system.alarmState'\n\n# Anything wrong with a sensor?\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq '[.sensors[] | select(.flags.offline or .flags.lowBattery) | {name, type, flags}]'\n\n# Recent activity\nss_api GET \"/subscriptions/$SID/events?numEvents=10\" \\\n  | jq -r '.events[] | \"\\(.eventTimestamp|todate)  \\(.info)\"'\n```\n\nMore recipes, including lock state and base-station health:\n[references/recipes.md](references/recipes.md).\n\n## Writes — read this before running one\n\nThese commands act on a **physical security system**. Disarming leaves a house\nunmonitored; arming can trip a siren and a monitoring-center dispatch; unlocking\nopens a real door. Confirm intent with the user before running any of them, and\nnever run one speculatively.\n\n```bash\nss_api POST \"/ss3/subscriptions/$SID/state/away\"      # or /home, /off — no body\nss_api POST \"/doorlock/$SID/<serial>/state\" '{\"state\":\"lock\"}'   # or \"unlock\"\n```\n\n**A 2xx is not proof it worked.** Re-read and check one field:\n\n```bash\nsleep 3\nss_api GET \"/users/$(ss_api GET /api/authCheck | jq -r .userId)/subscriptions?activeOnly=true\" \\\n  | jq -r '.subscriptions[0].location.system.alarmState'\n```\n\nFour things that make naive verification lie:\n\n- Arming reports `AWAY_COUNT` / `HOME_COUNT` while the exit delay runs, settling\n  to `AWAY` / `HOME`. That is success in progress, not failure.\n- Do **not** diff whole objects — `alarmStateTimestamp`, `stateUpdated` and\n  `lastUpdated` advance on their own, so any call would look successful.\n- **Re-read locks with `forceUpdate=true`.** The cached payload lags by minutes\n  and has been seen reporting a *jammed* lock as cleanly `unlocked`.\n- **A lock takes ~5 s to report**, so a 3-second check calls a successful unlock\n  a failure. Poll, don't sleep once.\n\n**Arming also LOCKS your doors.** Each lock's `setting` has `home`/`away` flags\nthat auto-lock on arm, and `homeToOff`/`awayToOff` controlling whether disarming\nunlocks them. With `homeToOff: 0` (common), arm-then-disarm leaves the doors\nlocked — so it is *not* a safe way to test arming. Check with:\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq -r '.sensors[] | select(.type==16) | \"\\(.name)\\t\\(.setting)\"'\n```\n\nA lock reporting `lockJamState: 1` will not respond to commands at all: the POST\nstill returns `200`, and nothing moves. That is a hardware fault, not a bad\nrequest.\n\n## PINs are cleartext\n\n`/ss3/subscriptions/$SID/settings/normal` returns `settings.pins` — the master,\nduress and named-user **alarm codes, in cleartext**, in the same payload as the\nharmless settings. Project `.settings.normal` unless the user explicitly wants\nthe codes:\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/settings/normal?forceUpdate=false\" | jq '.settings.normal'\n```\n\n## Notes\n\n- `forceUpdate=true` makes the base station re-poll its devices — slower and\n  harder on the hardware. Leave it `false` unless freshness matters.\n- Lock state encoding is counter-intuitive: `lockState` **1 = locked, 2 =\n  unlocked**, and `lockJamState` overrides both.\n- Device type ids `21`, `23` and `24` appear on real systems but aren't in any\n  public enum — handle unknown types gracefully.\n- Avoid tight polling; the API rate-limits.\n\nFull verified endpoint reference: `~/git/simplisafe-mcp/docs/SIMPLISAFE-API.md`.\n\nFile v1.1.2:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"simplisafe-mcp\",\n  \"version\": \"1.1.2\",\n  \"publishedAt\": 1790177971562\n}\n\nFile v1.1.2:references/recipes.md\n\n# SimpliSafe curl + jq recipes\n\nAll examples assume:\n\n```bash\nsource ~/git/simplisafe-mcp/skills/simplisafe-api/references/ss-helpers.sh\nSID=$(ss_sid)\nUID_=$(ss_api GET /api/authCheck | jq -r .userId)\n```\n\nEvery recipe below was run against a live account.\n\n## System\n\n### Full system state\n\n```bash\nss_api GET \"/users/$UID_/subscriptions?activeOnly=true\" \\\n  | jq '.subscriptions[] | {\n      sid,\n      name: .location.locationName,\n      state: .location.system.alarmState,\n      alarming: .location.system.isAlarming,\n      offline: .location.system.isOffline,\n      powerOutage: .location.system.powerOutage,\n      conn: .location.system.connType,\n      version: .location.system.version\n    }'\n```\n\n### One-line \"is the house armed?\"\n\n```bash\nss_api GET \"/users/$UID_/subscriptions?activeOnly=true\" \\\n  | jq -r '.subscriptions[0].location.system | \"\\(.alarmState)\\(if .isAlarming then \"  ** ALARMING **\" else \"\" end)\"'\n```\n\n### Base-station messages (firmware notices, faults)\n\n```bash\nss_api GET \"/users/$UID_/subscriptions?activeOnly=true\" \\\n  | jq -r '.subscriptions[0].location.system.messages[] | \"\\(.timestamp|todate)  \\(.text)\"'\n```\n\n## Sensors\n\n### All devices, compact\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq -r '.sensors[] | \"\\(.type)\\t\\(.name)\\tlowBat=\\(.flags.lowBattery)\\toffline=\\(.flags.offline)\"' \\\n  | column -t\n```\n\n### Only devices needing attention\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq '[.sensors[]\n         | select(.flags.offline or .flags.lowBattery or (.status.triggered // false))\n         | {name, type, offline: .flags.offline, lowBattery: .flags.lowBattery, triggered: .status.triggered}]'\n```\n\n### Currently-open entry sensors (type 5)\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=true\" \\\n  | jq -r '.sensors[] | select(.type==5 and .status.triggered==true) | .name'\n```\n\n`forceUpdate=true` is warranted here — an open/closed reading is only meaningful\nif it is fresh.\n\n### Device type ids\n\n| id | device | | id | device |\n| --- | --- | --- | --- | --- |\n| 0 | remote | | 13 | siren |\n| 1 | keypad | | 14 | smoke + CO |\n| 2 | keychain | | 15 | doorbell |\n| 3 | panic button | | 16 | **lock** |\n| 4 | motion | | 17 | outdoor camera |\n| 5 | **entry** | | 20 | motion v2 |\n| 6 | glass break | | 22 | outdoor bell box |\n| 7 | carbon monoxide | | 253 | lock keypad |\n| 8 | smoke | | | |\n| 9 | leak | | 21, 23, 24 | seen live, **not in any public enum** |\n| 10 | temperature | | | |\n| 12 | camera | | | |\n\nType 24 carries `smokeTriggered` / `coTriggered` / `tamper` / `endOfLife`.\n\n## Locks\n\n### Lock roster with decoded state\n\n`lockState` **1 = locked, 2 = unlocked**; `lockJamState` overrides both.\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq -r '.sensors[] | select(.type==16) |\n      \"\\(.name)\\t\\(if .status.lockJamState==1 then \"JAMMED\"\n                   elif .status.lockState==1 then \"locked\"\n                   else \"unlocked\" end)\\tlowBat=\\(.status.lockLowBattery)\\tpinPadOffline=\\(.status.pinPadOffline)\"' \\\n  | column -t\n```\n\n### Serials, for the write commands\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq -r '.sensors[] | select(.type==16) | \"\\(.name)\\t\\(.serial)\"'\n```\n\n## Events\n\n### Recent activity, human-readable\n\n```bash\nss_api GET \"/subscriptions/$SID/events?numEvents=20\" \\\n  | jq -r '.events[] | \"\\(.eventTimestamp|todate)  [\\(.eventType)]  \\(.info)  \\(.sensorName // \"\")\"'\n```\n\nNote: **no `ss3/` prefix** on this route, and `eventTimestamp` is in seconds.\n\n> **`numEvents` maxes out at 50.** Verified by bisection against the live API:\n> 50 succeeds, 51 and above return `400 InvalidParameter`. To reach further back,\n> page with `fromTimestamp` rather than asking for more at once.\n\n### Only alarms\n\n```bash\nss_api GET \"/subscriptions/$SID/events?numEvents=50\" \\\n  | jq -r '.events[] | select(.eventType==\"alarm\") | \"\\(.eventTimestamp|todate)  \\(.info)\"'\n```\n\n### Since a point in time\n\n```bash\nSINCE=$(date -v-24H +%s)     # macOS; GNU: date -d '24 hours ago' +%s\nss_api GET \"/subscriptions/$SID/events?fromTimestamp=$SINCE&numEvents=50\" \\\n  | jq -r '.events[] | \"\\(.eventTimestamp|todate)  \\(.info)\"'\n```\n\n### Who disarmed it, and when\n\n```bash\nss_api GET \"/subscriptions/$SID/events?numEvents=50\" \\\n  | jq -r '.events[] | select(.info | test(\"Disarm\"; \"i\")) | \"\\(.eventTimestamp|todate)  \\(.info)  by \\(.pinName // \"unknown\")\"'\n```\n\n## Settings and health\n\n### Delays and volumes (PIN block excluded)\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/settings/normal?forceUpdate=false\" \\\n  | jq '.settings.normal | {entryDelayHome, entryDelayAway, exitDelayHome, exitDelayAway,\n                            alarmVolume, alarmDuration, doorChime, voicePrompts}'\n```\n\n### Base-station health\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/settings/normal?forceUpdate=false\" \\\n  | jq '.basestationStatus | {wifiStatus, wifiRssi, wallPower, backupBattery,\n                              gsmStatus, gsmRssi, rfJamming, cellCarrier}'\n```\n\n### PINs — cleartext, ask first\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/settings/normal?forceUpdate=false\" | jq '.settings.pins'\n```\n\nReturns the master and duress codes and every named user PIN in the clear. Only\nrun it if the user actually wants the codes; `.settings.normal` above is the\nsafe projection for everything else.\n\n## Writes\n\nConfirm with the user first — these move real hardware.\n\n```bash\n# Arm / disarm (no request body)\nss_api POST \"/ss3/subscriptions/$SID/state/away\"\nss_api POST \"/ss3/subscriptions/$SID/state/home\"\nss_api POST \"/ss3/subscriptions/$SID/state/off\"\n\n# Locks (serial from the roster above)\nss_api POST \"/doorlock/$SID/<serial>/state\" '{\"state\":\"lock\"}'\nss_api POST \"/doorlock/$SID/<serial>/state\" '{\"state\":\"unlock\"}'\n```\n\n### Verify, don't assume\n\n```bash\narm_and_verify() {\n  local want=\"$1\"\n  ss_api POST \"/ss3/subscriptions/$SID/state/$want\" >/dev/null || return 1\n  sleep 3\n  local got\n  got=$(ss_api GET \"/users/$UID_/subscriptions?activeOnly=true\" \\\n        | jq -r '.subscriptions[0].location.system.alarmState')\n  case \"$got\" in\n    \"$(echo \"$want\" | tr '[:lower:]' '[:upper:]')\")       echo \"confirmed: $got\" ;;\n    \"$(echo \"$want\" | tr '[:lower:]' '[:upper:]')_COUNT\") echo \"in progress (exit delay): $got\" ;;\n    *) echo \"UNCONFIRMED: wanted $want, system reports $got\" >&2; return 1 ;;\n  esac\n}\n```\n\nThe `_COUNT` case is the exit delay counting down — a success, not a failure.\nComparing whole objects instead of this one field would report success\nunconditionally, because the timestamps advance on their own.\n\nFile v1.1.2:skill-card.md\n\n## Description:\n\nQuery and control a SimpliSafe alarm system from the shell with curl, including system state, sensors, locks, events, settings, arm/disarm, and lock/unlock workflows.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nExternal users and developers use this skill to ask an agent to inspect SimpliSafe alarm, sensor, lock, event, and settings data and to prepare or run authenticated shell commands for allowed control actions.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: The skill can disarm an alarm system or unlock a door through authenticated shell calls.\n\nMitigation: Require explicit user confirmation before disarm or unlock actions, avoid speculative writes, and verify the final device state after the command.\n\nRisk: A long-lived SimpliSafe refresh token and cached access token can read household security data and change alarm or lock state.\n\nMitigation: Keep the refresh token and token cache private, avoid shared machines, and revoke the refresh token by signing out of all devices if exposure is suspected.\n\nRisk: Settings endpoints can return master, duress, and named-user alarm PINs in cleartext.\n\nMitigation: Use safe projections that exclude PIN blocks unless the user explicitly asks for codes, and avoid logging or displaying codes unnecessarily.\n\nRisk: A successful HTTP response may not prove that a physical alarm or lock changed state, and cached lock data can be stale.\n\nMitigation: Re-read targeted state fields, request fresh lock state when freshness matters, and poll for lock updates instead of relying on a single fixed delay.\n\n## Reference(s):\n\n- [SimpliSafe curl + jq recipes](references/recipes.md)\n- [SimpliSafe shell helpers](references/ss-helpers.sh)\n- [ClawHub skill listing](https://clawhub.ai/chrischall/skills/simplisafe-mcp)\n\n## Skill Output:\n\n**Output Type(s):** [text, markdown, shell commands, configuration, guidance]\n\n**Output Format:** [Markdown with inline bash code blocks and JSON command output guidance]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Requires curl, jq, and a private SIMPLISAFE_REFRESH_TOKEN or configured local environment file.]\n\n## Skill Version(s):\n\n1.1.2 (source: ClawHub release evidence)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment.\n\nArchive v1.1.1: 5 files, 9931 bytes\n\nFiles: references/recipes.md (6629b), references/ss-helpers.sh (5486b), skill-card.md (2552b), SKILL.md (6268b), _meta.json (133b)\n\nFile v1.1.1:SKILL.md\n\n---\nname: simplisafe-api\ndescription: Query and control a SimpliSafe alarm system from the shell with curl — read system state, sensors, locks, events and settings, and arm/disarm or lock/unlock. Use when the user asks about their SimpliSafe alarm, house sensors, door locks, or whether the system is armed. Requires a one-time browser login to mint a refresh token.\n---\n\n# SimpliSafe from the shell\n\nSimpliSafe's API is reachable **server-side** — no browser bridge, no extension,\nno signed-in tab. Auth is a bearer token minted from a long-lived OAuth refresh\ntoken, so everything here is plain `curl` + `jq`.\n\nRequires `curl` and `jq`.\n\n## One-time setup\n\nSimpliSafe issues no API keys. The credential is an OAuth refresh token, minted\nby a browser login you do **once**:\n\n```bash\nnode ~/git/simplisafe-mcp/scripts/bootstrap-auth.mjs           # prints an authorize URL\n# sign in, then copy the com.simplisafe.mobile://... URL the browser fails to open\nnode ~/git/simplisafe-mcp/scripts/bootstrap-auth.mjs \"<that URL>\"\n```\n\nThat writes `SIMPLISAFE_REFRESH_TOKEN` to `~/git/simplisafe-mcp/.env` (mode 0600).\nSimpliSafe does **not** rotate refresh tokens, so this survives indefinitely —\nuntil you sign out of all devices in the SimpliSafe app, which revokes it.\n\nTo capture the code from the browser: open DevTools → Network → tick **Preserve\nlog** *before* signing in, then find the failed navigation to\n`com.simplisafe.mobile://…?code=…` and copy its link address. The code is\nsingle-use and expires in ~2 minutes.\n\n## Core pattern\n\n```bash\nsource ~/git/simplisafe-mcp/skills/simplisafe-api/references/ss-helpers.sh\n\nss_api GET /api/authCheck | jq            # who am I\nSID=$(ss_sid)                             # the single active system id\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" | jq\n```\n\n`ss_api <METHOD> <PATH> [JSON_BODY]` attaches auth, prints the body on stdout,\nand on a non-2xx prints to **stderr** and returns 1 — so a failure never looks\nlike an empty result. Access tokens are cached in `$TMPDIR` (0600) and re-minted\nonly when stale; the helpers never write to the repo's `.env`.\n\n## Resolve first\n\nAlmost every path needs a **system id (`sid`)**, and getting one takes two calls\n(`authCheck` → `userId`, then the subscriptions route). `ss_sid` does both, and\n**fails loudly if the account has more than one system** rather than guessing —\npass the sid explicitly in that case.\n\nNote two routing traps:\n\n- The system version that decides `ss3/` routing is at\n  `location.system.version`, **not** the top-level `systemVersion` (a different\n  number entirely).\n- **Events and doorlock control are NOT under the `ss3/` prefix.** Everything\n  else is.\n\n## Reads\n\n```bash\n# Is it armed? -> OFF | HOME | AWAY | HOME_COUNT | AWAY_COUNT | ALARM\nss_api GET \"/users/$(ss_api GET /api/authCheck | jq -r .userId)/subscriptions?activeOnly=true\" \\\n  | jq -r '.subscriptions[0].location.system.alarmState'\n\n# Anything wrong with a sensor?\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq '[.sensors[] | select(.flags.offline or .flags.lowBattery) | {name, type, flags}]'\n\n# Recent activity\nss_api GET \"/subscriptions/$SID/events?numEvents=10\" \\\n  | jq -r '.events[] | \"\\(.eventTimestamp|todate)  \\(.info)\"'\n```\n\nMore recipes, including lock state and base-station health:\n[references/recipes.md](references/recipes.md).\n\n## Writes — read this before running one\n\nThese commands act on a **physical security system**. Disarming leaves a hou","readmeExcerpt":"Skill: simplisafe-mcp Owner: chrischall Summary: Query and control a SimpliSafe alarm system from the shell with curl — read system state, sensors, locks, events and settings, and arm/disarm or lock/unlock. Use when the user asks about their SimpliSafe alarm, house sensors, door locks, or whether the system is armed. Requires a one-time browser login to mint a refresh token. Tags: latest:1.3.1 Version history: v1.3.1","codeSnippets":[],"executableExamples":[{"language":"bash","snippet":"node ~/git/simplisafe-mcp/scripts/bootstrap-auth.mjs           # prints an authorize URL\n# sign in, then copy the com.simplisafe.mobile://... URL the browser fails to open\nnode ~/git/simplisafe-mcp/scripts/bootstrap-auth.mjs \"<that URL>\""},{"language":"bash","snippet":"source ~/git/simplisafe-mcp/skills/simplisafe-api/references/ss-helpers.sh\n\nss_api GET /api/authCheck | jq            # who am I\nSID=$(ss_sid)                             # the single active system id\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" | jq"},{"language":"bash","snippet":"# Is it armed? -> OFF | HOME | AWAY | HOME_COUNT | AWAY_COUNT | ALARM\nss_api GET \"/users/$(ss_api GET /api/authCheck | jq -r .userId)/subscriptions?activeOnly=true\" \\\n  | jq -r '.subscriptions[0].location.system.alarmState'\n\n# Anything wrong with a sensor?\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq '[.sensors[] | select(.flags.offline or .flags.lowBattery) | {name, type, flags}]'\n\n# Recent activity\nss_api GET \"/subscriptions/$SID/events?numEvents=10\" \\\n  | jq -r '.events[] | \"\\(.eventTimestamp|todate)  \\(.info)\"'"},{"language":"bash","snippet":"ss_api POST \"/ss3/subscriptions/$SID/state/away\"      # or /home, /off — no body\nss_api POST \"/doorlock/$SID/<serial>/state\" '{\"state\":\"lock\"}'   # or \"unlock\""},{"language":"bash","snippet":"sleep 3\nss_api GET \"/users/$(ss_api GET /api/authCheck | jq -r .userId)/subscriptions?activeOnly=true\" \\\n  | jq -r '.subscriptions[0].location.system.alarmState'"},{"language":"bash","snippet":"ss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq -r '.sensors[] | select(.type==16) | \"\\(.name)\\t\\(.setting)\"'"}],"parameters":null,"dependencies":[],"permissions":[],"extractedFiles":[{"path":"SKILL.md","content":"---\nname: simplisafe-api\ndescription: Query and control a SimpliSafe alarm system from the shell with curl — read system state, sensors, locks, events and settings, and arm/disarm or lock/unlock. Use when the user asks about their SimpliSafe alarm, house sensors, door locks, or whether the system is armed. Requires a one-time browser login to mint a refresh token.\n---\n\n# SimpliSafe from the shell\n\nSimpliSafe's API is reachable **server-side** — no browser bridge, no extension,\nno signed-in tab. Auth is a bearer token minted from a long-lived OAuth refresh\ntoken, so everything here is plain `curl` + `jq`.\n\nRequires `curl` and `jq`.\n\n## One-time setup\n\nSimpliSafe issues no API keys. The credential is an OAuth refresh token, minted\nby a browser login you do **once**:\n\n```bash\nnode ~/git/simplisafe-mcp/scripts/bootstrap-auth.mjs           # prints an authorize URL\n# sign in, then copy the com.simplisafe.mobile://... URL the browser fails to open\nnode ~/git/simplisafe-mcp/scripts/bootstrap-auth.mjs \"<that URL>\"\n```\n\nThat writes `SIMPLISAFE_REFRESH_TOKEN` to `~/git/simplisafe-mcp/.env` (mode 0600).\nSimpliSafe does **not** rotate refresh tokens, so this survives indefinitely —\nuntil you sign out of all devices in the SimpliSafe app, which revokes it.\n\nTo capture the code from the browser: open DevTools → Network → tick **Preserve\nlog** *before* signing in, then find the failed navigation to\n`com.simplisafe.mobile://…?code=…` and copy its link address. The code is\nsingle-use and expires in ~2 minutes.\n\n## Core pattern\n\n```bash\nsource ~/git/simplisafe-mcp/skills/simplisafe-api/references/ss-helpers.sh\n\nss_api GET /api/authCheck | jq            # who am I\nSID=$(ss_sid)                             # the single active system id\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" | jq\n```\n\n`ss_api <METHOD> <PATH> [JSON_BODY]` attaches auth, prints the body on stdout,\nand on a non-2xx prints to **stderr** and returns 1 — so a failure never looks\nlike an empty result. Access tokens are cached in `$TMPDIR` (0600) and re-minted\nonly when stale; the helpers never write to the repo's `.env`.\n\n## Resolve first\n\nAlmost every path needs a **system id (`sid`)**, and getting one takes two calls\n(`authCheck` → `userId`, then the subscriptions route). `ss_sid` does both, and\n**fails loudly if the account has more than one system** rather than guessing —\npass the sid explicitly in that case.\n\nNote two routing traps:\n\n- The system version that decides `ss3/` routing is at\n  `location.system.version`, **not** the top-level `systemVersion` (a different\n  number entirely).\n- **Events and doorlock control are NOT under the `ss3/` prefix.** Everything\n  else is.\n\n## Reads\n\n```bash\n# Is it armed? -> OFF | HOME | AWAY | HOME_COUNT | AWAY_COUNT | ALARM\nss_api GET \"/users/$(ss_api GET /api/authCheck | jq -r .userId)/subscriptions?activeOnly=true\" \\\n  | jq -r '.subscriptions[0].location.system.alarmState'\n\n# Anything wrong with a sensor?\nss_api GET \"/ss3/subscriptions/$SID"},{"path":"_meta.json","content":"{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"simplisafe-mcp\",\n  \"version\": \"1.3.1\",\n  \"publishedAt\": 1791588444696\n}"},{"path":"references/recipes.md","content":"# SimpliSafe curl + jq recipes\n\nAll examples assume:\n\n```bash\nsource ~/git/simplisafe-mcp/skills/simplisafe-api/references/ss-helpers.sh\nSID=$(ss_sid)\nUID_=$(ss_api GET /api/authCheck | jq -r .userId)\n```\n\nEvery recipe below was run against a live account.\n\n## System\n\n### Full system state\n\n```bash\nss_api GET \"/users/$UID_/subscriptions?activeOnly=true\" \\\n  | jq '.subscriptions[] | {\n      sid,\n      name: .location.locationName,\n      state: .location.system.alarmState,\n      alarming: .location.system.isAlarming,\n      offline: .location.system.isOffline,\n      powerOutage: .location.system.powerOutage,\n      conn: .location.system.connType,\n      version: .location.system.version\n    }'\n```\n\n### One-line \"is the house armed?\"\n\n```bash\nss_api GET \"/users/$UID_/subscriptions?activeOnly=true\" \\\n  | jq -r '.subscriptions[0].location.system | \"\\(.alarmState)\\(if .isAlarming then \"  ** ALARMING **\" else \"\" end)\"'\n```\n\n### Base-station messages (firmware notices, faults)\n\n```bash\nss_api GET \"/users/$UID_/subscriptions?activeOnly=true\" \\\n  | jq -r '.subscriptions[0].location.system.messages[] | \"\\(.timestamp|todate)  \\(.text)\"'\n```\n\n## Sensors\n\n### All devices, compact\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq -r '.sensors[] | \"\\(.type)\\t\\(.name)\\tlowBat=\\(.flags.lowBattery)\\toffline=\\(.flags.offline)\"' \\\n  | column -t\n```\n\n### Only devices needing attention\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq '[.sensors[]\n         | select(.flags.offline or .flags.lowBattery or (.status.triggered // false))\n         | {name, type, offline: .flags.offline, lowBattery: .flags.lowBattery, triggered: .status.triggered}]'\n```\n\n### Currently-open entry sensors (type 5)\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=true\" \\\n  | jq -r '.sensors[] | select(.type==5 and .status.triggered==true) | .name'\n```\n\n`forceUpdate=true` is warranted here — an open/closed reading is only meaningful\nif it is fresh.\n\n### Device type ids\n\n| id | device | | id | device |\n| --- | --- | --- | --- | --- |\n| 0 | remote | | 13 | siren |\n| 1 | keypad | | 14 | smoke + CO |\n| 2 | keychain | | 15 | doorbell |\n| 3 | panic button | | 16 | **lock** |\n| 4 | motion | | 17 | outdoor camera |\n| 5 | **entry** | | 20 | motion v2 |\n| 6 | glass break | | 22 | outdoor bell box |\n| 7 | carbon monoxide | | 253 | lock keypad |\n| 8 | smoke | | | |\n| 9 | leak | | 21, 23, 24 | seen live, **not in any public enum** |\n| 10 | temperature | | | |\n| 12 | camera | | | |\n\nType 24 carries `smokeTriggered` / `coTriggered` / `tamper` / `endOfLife`.\n\n## Locks\n\n### Lock roster with decoded state\n\n`lockState` **1 = locked, 2 = unlocked**; `lockJamState` overrides both.\n\n```bash\nss_api GET \"/ss3/subscriptions/$SID/sensors?forceUpdate=false\" \\\n  | jq -r '.sensors[] | select(.type==16) |\n      \"\\(.name)\\t\\(if .status.lockJamState==1 then \"JAMMED\"\n                   elif .status.lockState==1 then \"locked\"\n                   else \"unlo"},{"path":"skill-card.md","content":"## Description:\n\nHelps agents check SimpliSafe alarm, sensor, lock, and event status and issue alarm or lock commands through the shell.\n\nThis skill is ready for commercial/non-commercial use.\n\n## Publisher:\n\n[chrischall](https://clawhub.ai/user/chrischall)\n\n### License/Terms of Use:\n\nMIT-0\n\n## Use Case:\n\nSimpliSafe account holders and their agents can inspect home security status and, with explicit authorization, arm or disarm the alarm and control door locks.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Authenticated commands can disarm the alarm or unlock doors without an enforced confirmation step.\n\nMitigation: Require explicit human confirmation before every arm, disarm, lock, or unlock action; prefer a version that enforces write confirmation and restricts allowed actions.\n\nRisk: A long-lived refresh token can grant ongoing account access if exposed in logs, shell history, or shared files.\n\nMitigation: Keep the token private and avoid exposing credentials in logs, shell history, or shared files.\n\nRisk: The settings response can contain cleartext alarm PINs, and lock status can be stale after a write.\n\nMitigation: Request PINs only with explicit consent; exclude them from routine settings output and refresh lock status before reporting a change as complete.\n\n## Reference(s):\n\n- [ClawHub skill release](https://clawhub.ai/chrischall/skills/simplisafe-mcp)\n- [SimpliSafe command recipes](references/recipes.md)\n- [SimpliSafe shell helpers](references/ss-helpers.sh)\n\n## Skill Output:\n\n**Output Type(s):** [Shell commands, Guidance, Configuration instructions]\n\n**Output Format:** [Markdown with shell commands and JSON-query examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Requires an authenticated SimpliSafe account, curl, and jq.]\n\n## Skill Version(s):\n\n1.3.1 (source: ClawHub release metadata)\n\n## Ethical Considerations:\n\nUsers should evaluate whether this skill is appropriate for their environment, review any generated or modified files before relying on them, and apply their organization's safety, security, and compliance requirements before deployment."}],"languages":[],"docsSourceLabel":"CLAWHUB","editorialOverview":null,"editorialQuality":{"score":100,"threshold":65,"status":"thin","wordCount":1381,"uniquenessScore":44,"reasons":["uniqueness-below-45"]}},"media":{"evidence":{"source":"no-media","verified":false,"confidence":"low","updatedAt":"2026-10-10T12:03:05.666Z","emptyReason":"No screenshots, media assets, or demo links are available."},"primaryImageUrl":null,"mediaAssetCount":0,"assets":[],"demoUrl":null},"ownerResources":{"evidence":{"source":"unclaimed","verified":false,"confidence":"low","updatedAt":"2026-10-10T12:03:05.666Z","emptyReason":"This page has not been claimed by the agent owner."},"hasCustomPage":false,"customPageUpdatedAt":null,"customLinks":[],"structuredLinks":{"docsUrl":null,"demoUrl":null,"supportUrl":null,"pricingUrl":null,"statusUrl":null},"customPage":null},"relatedAgents":{"evidence":{"source":"protocol-neighbors","verified":false,"confidence":"medium","updatedAt":"2026-10-10T14:45:16.966Z","emptyReason":null},"items":[{"id":"8ebccd8e-3863-4187-8355-c3f14e1f9edf","entityType":"agent","canonicalPath":"/agent/iofficeai-aionui","slug":"iofficeai-aionui","name":"AionUi","description":"Free, local, open-source 24/7 Cowork app and OpenClaw for Gemini CLI, Claude Code, Codex, OpenCode, Qwen Code, Goose CLI, Auggie, and more | 🌟 Star if you like it!","url":"https://github.com/iOfficeAI/AionUi","homepage":"https://www.aionui.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-10-09T19:11:12.944Z","createdAt":"2026-02-25T03:38:16.584Z","downloads":null},{"id":"b917f68a-ebff-438e-84f8-3f4b2494c0bc","entityType":"agent","canonicalPath":"/agent/activepieces-activepieces","slug":"activepieces-activepieces","name":"activepieces","description":"AI Agents & MCPs & AI Workflow Automation • (~400 MCP servers for AI agents) • AI Automation / AI Agent with MCPs • AI Workflows & AI Agents • MCPs for AI Agents","url":"https://github.com/activepieces/activepieces","homepage":"https://www.activepieces.com","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-15T02:22:12.426Z","createdAt":"2026-02-25T03:38:12.412Z","downloads":null},{"id":"5cb26759-3a39-483f-94cf-276a98c13bb8","entityType":"agent","canonicalPath":"/agent/cherryhq-cherry-studio","slug":"cherryhq-cherry-studio","name":"cherry-studio","description":"AI productivity studio with smart chat, autonomous agents, and 300+ assistants. Unified access to frontier LLMs","url":"https://github.com/CherryHQ/cherry-studio","homepage":"https://cherry-ai.com","source":"GITHUB_REPOS","protocols":["MCP","OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-04-11T14:38:40.986Z","createdAt":"2026-02-25T03:38:19.379Z","downloads":null},{"id":"6f6582d0-5d76-4f0f-b81d-86520247950b","entityType":"agent","canonicalPath":"/agent/copilotkit-copilotkit","slug":"copilotkit-copilotkit","name":"CopilotKit","description":"The Frontend for Agents & Generative UI. React + Angular","url":"https://github.com/CopilotKit/CopilotKit","homepage":"https://docs.copilotkit.ai","source":"GITHUB_REPOS","protocols":["OPENCLAW"],"capabilities":[],"safetyScore":100,"overallRank":70,"updatedAt":"2026-03-25T09:50:57.846Z","createdAt":"2026-02-25T03:39:14.617Z","downloads":null}],"links":{"hub":"/agent","source":"/agent/source/clawhub","protocols":[{"label":"OpenClaw","href":"/agent/protocol/openclew"}]}}}