{"id":"4a484881-9950-4ce2-a861-927e2f69804f","entityType":"agent","slug":"clawhub-chrischall-kiaaccess-curl","name":"kiaaccess-curl","canonicalUrl":"https://www.xpersona.co/agent/clawhub-chrischall-kiaaccess-curl","canonicalPath":"/agent/clawhub-chrischall-kiaaccess-curl","generatedAt":"2026-10-10T08:45:32.271Z","source":"CLAWHUB","claimStatus":"UNCLAIMED","verificationTier":"NONE","summary":{"evidence":{"source":"editorial-content","verified":true,"confidence":"high","updatedAt":"2026-10-10T02:51:24.503Z","emptyReason":null},"description":"Query and command a Kia vehicle directly with curl against the Kia Owners API (api.owners.kia.com), without running the MCP server. Use when the user wants a one-off read of their Kia's status, location, or EV charge state, or to lock/unlock/start climate from the shell — \"check my Kia\", \"is the car locked\", \"what's the EV9 charge\", \"lock the car from the terminal\". Requires KIA_USERNAME/KIA_PASSWORD and a one-time SMS/email MFA bootstrap. Skill: kiaaccess-curl Owner: chrischall Summary: Query and command a Kia vehicle directly with curl against the Kia Owners API (api.owners.kia.com), without running the MCP server. Use when the user wants a one-off read of their Kia's status, location, or EV charge state, or to lock/unlock/start climate from the shell — \"check my Kia\", \"is the car locked\", \"what's the EV9 charge\", \"lock the car from the terminal\". Re","descriptionLabel":"Technical summary","evidenceSummary":"Capability contract not published. No trust telemetry is available yet. 1.8K downloads reported by the source. Last updated 10/10/2026.","installCommand":"clawhub skill install s17cjx1a349nz5apaqp02vgz4h85728z:kiaaccess-curl","sourceUrl":"https://clawhub.ai/chrischall/kiaaccess-curl","homepage":"https://clawhub.ai/chrischall/skills/kiaaccess-curl","primaryLinks":[{"label":"View on ClawHub","url":"https://clawhub.ai/chrischall/kiaaccess-curl","kind":"source"},{"label":"Homepage","url":"https://clawhub.ai/chrischall/skills/kiaaccess-curl","kind":"homepage"}],"safetyScore":84,"overallRank":62,"popularityScore":65,"trustScore":null,"claimedByName":null,"isOwner":false,"seoDescription":"Query and command a Kia vehicle directly with curl against the Kia Owners API (api.owners.kia.com), without running the MCP server. Use when the user wants a on"},"coverage":{"evidence":{"source":"public-profile","verified":false,"confidence":"medium","updatedAt":"2026-10-10T02:51:24.503Z","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-10T02:51:24.503Z","emptyReason":null},"stars":null,"forks":null,"downloads":1756,"packageName":null,"latestVersion":"2.0.0","tractionLabel":"1.8K downloads"},"release":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"medium","updatedAt":"2026-10-10T02:51:24.472Z","emptyReason":null},"lastUpdatedAt":"2026-10-10T02:51:24.503Z","lastCrawledAt":"2026-10-10T02:51:24.472Z","lastIndexedAt":null,"nextCrawlAt":"2026-10-11T02:51:24.472Z","lastVerifiedAt":null,"highlights":[{"version":"2.0.0","createdAt":"2026-10-09T23:24:03.714Z","changelog":"kiaaccess-curl 2.0.0 - Removed the sample file skill-card.md. - No changes to functionality or documentation content.","fileCount":4,"zipByteSize":8255},{"version":"1.1.5","createdAt":"2026-10-07T13:35:27.254Z","changelog":"- Removed the file skill-card.md from the project. - No changes were made to skill features, behavior, or documentation content.","fileCount":4,"zipByteSize":8488},{"version":"1.1.4","createdAt":"2026-10-05T02:51:04.042Z","changelog":"- Removed the sample skill card file (skill-card.md) from the project. - No changes to functionality or usage; documentation and implementation remain the same.","fileCount":4,"zipByteSize":8305},{"version":"1.1.3","createdAt":"2026-10-03T01:41:44.627Z","changelog":"- Removed the sample file skill-card.md. - No user-facing functionality or documentation changes.","fileCount":4,"zipByteSize":8285},{"version":"1.1.2","createdAt":"2026-09-30T16:57:24.839Z","changelog":"- Removed the sample skill card file (skill-card.md). - No impact on usage or functionality.","fileCount":4,"zipByteSize":8359},{"version":"1.1.1","createdAt":"2026-09-25T16:03:35.019Z","changelog":"- Removed the skill-card.md file. - No functional or behavioral changes to the skill itself.","fileCount":4,"zipByteSize":8357},{"version":"1.1.0","createdAt":"2026-09-24T15:11:38.014Z","changelog":"- Removed the file: skill-card.md. - No functional changes to the skill logic or documentation.","fileCount":4,"zipByteSize":8541},{"version":"1.0.3","createdAt":"2026-09-23T21:39:01.783Z","changelog":"- Removed the file: skill-card.md - No changes to core functionality or usage. - Documentation and behavior remain unchanged.","fileCount":4,"zipByteSize":8407}]},"execution":{"evidence":{"source":"CLAWHUB","verified":false,"confidence":"low","updatedAt":null,"emptyReason":"No published capability contract is available yet."},"installCommand":"clawhub skill install s17cjx1a349nz5apaqp02vgz4h85728z:kiaaccess-curl","setupComplexity":"low","setupSteps":["Setup complexity is classified as HIGH. You must provision dedicated cloud infrastructure or an isolated VM. Do not run this directly on your local workstation.","Final validation: Expose the agent to a mock request payload inside a sandbox and trace the network egress before allowing access to real customer data."],"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-kiaaccess-curl/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-kiaaccess-curl/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-kiaaccess-curl/trust"},"curlExamples":["curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-kiaaccess-curl/snapshot\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-kiaaccess-curl/contract\"","curl -s \"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-kiaaccess-curl/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-10T08:45:32.269Z"}},"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-kiaaccess-curl/dossier","snapshotUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-kiaaccess-curl/snapshot","contractUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-kiaaccess-curl/contract","trustUrl":"https://www.xpersona.co/api/v1/agents/clawhub-chrischall-kiaaccess-curl/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":"high","updatedAt":"2026-10-10T02:51:24.503Z","emptyReason":null},"readme":"Skill: kiaaccess-curl\n\nOwner: chrischall\n\nSummary: Query and command a Kia vehicle directly with curl against the Kia Owners API (api.owners.kia.com), without running the MCP server. Use when the user wants a one-off read of their Kia's status, location, or EV charge state, or to lock/unlock/start climate from the shell — \"check my Kia\", \"is the car locked\", \"what's the EV9 charge\", \"lock the car from the terminal\". Requires KIA_USERNAME/KIA_PASSWORD and a one-time SMS/email MFA bootstrap.\n\nTags: latest:2.0.0\n\nVersion history:\n\nv2.0.0 | 2026-10-09T23:24:03.714Z | auto\n\nkiaaccess-curl 2.0.0\n\n- Removed the sample file skill-card.md.\n- No changes to functionality or documentation content.\n\nv1.1.5 | 2026-10-07T13:35:27.254Z | auto\n\n- Removed the file skill-card.md from the project.\n- No changes were made to skill features, behavior, or documentation content.\n\nv1.1.4 | 2026-10-05T02:51:04.042Z | auto\n\n- Removed the sample skill card file (skill-card.md) from the project.\n- No changes to functionality or usage; documentation and implementation remain the same.\n\nv1.1.3 | 2026-10-03T01:41:44.627Z | auto\n\n- Removed the sample file skill-card.md.\n- No user-facing functionality or documentation changes.\n\nv1.1.2 | 2026-09-30T16:57:24.839Z | auto\n\n- Removed the sample skill card file (skill-card.md).\n- No impact on usage or functionality.\n\nv1.1.1 | 2026-09-25T16:03:35.019Z | auto\n\n- Removed the skill-card.md file.  \n- No functional or behavioral changes to the skill itself.\n\nv1.1.0 | 2026-09-24T15:11:38.014Z | auto\n\n- Removed the file: skill-card.md.\n- No functional changes to the skill logic or documentation.\n\nv1.0.3 | 2026-09-23T21:39:01.783Z | auto\n\n- Removed the file: skill-card.md\n- No changes to core functionality or usage.\n- Documentation and behavior remain unchanged.\n\nv1.0.2 | 2026-09-23T15:42:59.682Z | auto\n\n- Removed the sample file skill-card.md.\n- No changes made to implementation or documentation beyond file removal.\n\nv1.0.1 | 2026-09-21T04:12:45.939Z | auto\n\n- Removed the documentation file skill-card.md.\n- No changes to core functionality; documentation only.\n\nv1.0.0 | 2026-09-20T02:49:37.302Z | auto\n\n- Removed the sample file skill-card.md.\n- No functional changes to the skill; documentation and usage remain unchanged.\n\nv0.9.0 | 2026-09-17T23:35:39.485Z | auto\n\n- Removed the sample file skill-card.md.\n- No changes to core functionality or documentation content.\n- Cleanup prepares for future updates; no user action required.\n\nv0.8.2 | 2026-09-14T18:49:54.894Z | auto\n\n- Removed the file skill-card.md.\n- No user-facing feature or functionality changes.\n\nv0.8.1 | 2026-09-10T17:50:27.402Z | auto\n\n- Removed the file skill-card.md.\n- No changes to skill logic or documentation content.\n\nv0.8.0 | 2026-09-04T22:21:13.795Z | auto\n\n- Removed the skill-card.md file.\n- No user-facing or functionality changes to the skill itself.\n\nv0.7.0 | 2026-08-31T16:37:54.602Z | auto\n\n- Removed the sample file skill-card.md.\n- No functional or documentation changes within SKILL.md; package remains unchanged except for the file removal.\n\nv0.6.2 | 2026-08-28T11:34:57.542Z | auto\n\n- skill-card.md file removed to clean up repository.\n- No user-facing or functional changes; documentation and usage remain the same.\n\nv0.6.1 | 2026-08-11T14:19:38.862Z | auto\n\n- Removed the sample file skill-card.md.\n- No changes to logic, functionality, or documentation; packaging cleanup only.\n\nv0.6.0 | 2026-08-10T19:30:57.549Z | auto\n\n- Removed the file skill-card.md.\n- No user-facing or functional changes; documentation and usage remain the same.\n\nv0.5.1 | 2026-08-09T21:02:21.783Z | auto\n\n- Removed the skill-card.md file.\n- No changes to functionality or documentation outside of the file removal.\n\nv0.5.0 | 2026-07-28T23:29:42.035Z | auto\n\n- Removed the file skill-card.md.\n- No code or logic changes; documentation and usage remain the same.\n\nv0.4.1 | 2026-07-28T21:52:07.957Z | auto\n\n- Removed the skill-card.md file.\n- No changes to code or functionality; documentation only.\n\nv0.4.0 | 2026-07-28T19:03:09.592Z | auto\n\n- Removed the sample file skill-card.md.\n- No functional changes to the skill logic or interface.\n- Documentation and usage details remain unchanged.\n\nv0.3.0 | 2026-07-28T14:34:46.113Z | auto\n\n- Removed the sample file skill-card.md.\n- No changes to core functionality or documentation besides removing the skill-card.\n\nv0.2.0 | 2026-07-28T11:24:02.232Z | auto\n\nSummary: Adds detailed usage instructions and safety notes for direct Kia API access via curl.\n\n- Added comprehensive setup and authentication instructions for accessing the Kia Owners API with curl.\n- Clarified the need for RFC-1123 date headers and body-based success checks.\n- Documented step-by-step MFA bootstrapping and session token management for secure, repeatable use.\n- Included endpoint coverage for vehicle status, EV charge state, and remote commands like door lock/unlock and climate start/stop.\n- Provided field references for verifying command completion reliably.\n- Clearly specified differences from using the `kiaaccess-mcp` server and stressed credential safety and prompt handling.\n\nArchive index:\n\nArchive v2.0.0: 4 files, 8255 bytes\n\nFiles: references/requests.md (9510b), skill-card.md (1769b), SKILL.md (4737b), _meta.json (133b)\n\nFile v2.0.0:SKILL.md\n\n---\nname: kiaaccess-curl\ndescription: Query and command a Kia vehicle directly with curl against the Kia Owners API (api.owners.kia.com), without running the MCP server. Use when the user wants a one-off read of their Kia's status, location, or EV charge state, or to lock/unlock/start climate from the shell — \"check my Kia\", \"is the car locked\", \"what's the EV9 charge\", \"lock the car from the terminal\". Requires KIA_USERNAME/KIA_PASSWORD and a one-time SMS/email MFA bootstrap.\n---\n\n# Kia Owners API via curl\n\nThe Kia Access app's API is reachable server-side — no browser, no bridge, no\nextension. This skill talks to it directly with `curl`.\n\nPrefer the `kiaaccess-mcp` server for anything conversational or repeated; use\nthis for one-off shell work, debugging, or when the server isn't running.\n\n**Ready-to-run request bodies and `jq` recipes: `references/requests.md`.**\nFull shape reference (verified live): `../../docs/KIA-API.md`.\n\n## Setup\n\n```bash\nexport KIA_USERNAME='you@example.com'\nexport KIA_PASSWORD='…'\nexport KIA_DEVICE=$(uuidgen)        # keep this stable across runs\n```\n\n## Two rules that will bite you\n\n1. **Every request needs an RFC-1123 `date` header.** Omit it and you get\n   `errorCode 9200 \"Missing mandatory data in header\"` — a message that does not\n   name the culprit. Regenerate it per request; a stale one is rejected.\n2. **HTTP is 200 even on failure.** Success is `status.statusCode == 0` in the\n   *body*. Never branch on the HTTP code.\n\nSource `references/requests.md`'s `kia_headers` helper rather than hand-rolling\nheaders — it handles both.\n\n## Auth: one-time MFA, then silent refresh\n\n`authUser` → `sendOTP` → `verifyOTP` yields a **`sid`** (session, short-lived)\nand an **`rmtoken`** (refresh, durable), both as *response headers*.\n\nAfterwards, `authUser` with the `rmtoken` header mints a fresh `sid` with **no\nMFA**. So you do the SMS dance once and then never again — save the `rmtoken`.\n\n> **Never retry a rejected login.** `errorCode 1001` (bad credentials) or `1037`\n> (bad email) increments `payload.loginAttempt`; enough failures set\n> `enforceRecaptcha` and **permanently break shell-based login**. Fix the\n> credential and try once.\n\nStore the `rmtoken` at `$KIA_SESSION` (default `~/.kiaaccess-mcp/curl-session.json`)\nwith `chmod 600`. It is a credential: it re-authenticates the account without a\npassword prompt.\n\n> Do **not** write it to `~/.kiaaccess-mcp/session.json`. That path belongs to the\n> `kiaaccess-mcp` server, whose store is keyed by `accountId` with a different\n> schema — overwriting it corrupts the server's session and forces it back\n> through MFA.\n\n## Calling\n\nReads and commands take `sid` (+ `vinkey` for vehicle-scoped calls). Get the\n`vinkey` from `ownr/gvl` → `payload.vehicleSummary[0].vehicleKey`.\n\n| Want | Endpoint |\n| --- | --- |\n| vehicles | `GET ownr/gvl` |\n| status (cached) | `POST cmm/gvi` |\n| status (force refresh) | `POST rems/rvs` |\n| EV charge targets | `GET evc/gts` |\n| lock / unlock | `GET rems/door/lock` / `rems/door/unlock` |\n| climate on / off | `POST rems/start` / `GET rems/stop` |\n| charge start / stop / limits | `POST evc/charge` / `GET evc/cancel` / `POST evc/sts` |\n\n## Confirming a command actually worked\n\n**A `statusCode: 0` means \"accepted\", not \"done\".**\n\n- **Do not poll `cmm/gts`.** Despite taking an `xid`, it returns global flags and\n  never reports per-action completion — polled through a real lock it never\n  changed.\n- **Re-read `cmm/gvi` and diff the field.** That is the only proof. Allow ~30–60s.\n\nFields to diff:\n\n| Command | Field |\n| --- | --- |\n| lock / unlock | `vehicleStatus.doorLock` |\n| climate on / off | `vehicleStatus.climate.airCtrl` and `vehicleStatus.ign3` |\n\nThree traps when writing that comparison:\n\n- **`syncDate` advances on every read.** Include it and *every* command looks\n  successful. Exclude it.\n- **There is no `airCtrlOn`.** Climate is nested under `vehicleStatus.climate`,\n  and the whole block is **absent** unless you request `cmm/gvi` with\n  `vehicleConfigReq.airTempRange: \"1\"` and `seatHeatCoolOption: \"1\"`.\n- **On an EV, `engine` stays `false`** with climate running — use `ign3`.\n\n## Verification status\n\nEvery endpoint here was verified live against a 2024 EV9 — all reads, the door\nand climate commands, and (against a plugged-in car) `evc/charge`, `evc/cancel`\nand `evc/sts`, each proven by a re-read rather than a 200.\n\nCharging proof fields live under `vehicleStatus.evStatus`: `batteryCharge`\n(true while charging), `batteryStatus` (SOC %), `batteryPlugin`. `evc/sts` is\nproven by re-reading `evc/gts`.\n\nOne caveat: **`rems/start`'s temperature may not apply** — a start requesting 70\nleft `airTemp.value` at 72. Treat it as best-effort.\n\nFile v2.0.0:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"kiaaccess-curl\",\n  \"version\": \"2.0.0\",\n  \"publishedAt\": 1791588243714\n}\n\nFile v2.0.0:references/requests.md\n\n# Ready-to-run requests\n\nEvery shape here was verified live on 2026-07-27 against a 2024 Kia EV9 —\nincluding the `evc/*` charging commands, run against the car while plugged in.\n\n## Header helper\n\nSource this first. It regenerates the mandatory `date` per call and keeps the\ndevice id stable.\n\n```bash\nKIA_BASE='https://api.owners.kia.com/apigw/v1'\n: \"${KIA_DEVICE:?export KIA_DEVICE=\\$(uuidgen) first}\"\n\n# Session file for THIS skill. Deliberately NOT ~/.kiaaccess-mcp/session.json —\n# that path belongs to the MCP server, whose store is keyed by accountId with a\n# different schema (src/session.ts). Writing this skill's flat\n# {rmtoken, deviceId} there corrupts the server's session and forces it back\n# through MFA.\nKIA_SESSION=\"${KIA_CURL_SESSION:-$HOME/.kiaaccess-mcp/curl-session.json}\"\n\n# Builds the header list into the KIA_HDRS array. Extra headers passed as args.\nkia_headers() {\n  local z sign off h\n  z=$(date +%z)                       # e.g. -0800, +0530\n  sign=${z:0:1}\n  # `10#` forces base 10. Without it, bash reads a leading-zero offset such as\n  # `08`/`09` as OCTAL and dies with \"value too great for base\" — so this breaks\n  # in US Pacific winter, Alaska, Japan, Korea and China. zsh does not have the\n  # problem, which is a good way to ship it broken without noticing.\n  off=$(( 10#${z:1:2} ))\n  [ \"$sign\" = \"-\" ] && off=$(( 0 - off ))\n\n  KIA_HDRS=()\n  for h in \\\n    \"content-type: application/json;charset=utf-8\" \\\n    \"accept: application/json\" \\\n    \"accept-language: en-US,en;q=0.9\" \\\n    \"accept-charset: utf-8\" \\\n    \"apptype: L\" \"appversion: 7.22.0\" \"clientid: SPACL716-APL\" \\\n    \"clientuuid: ${KIA_DEVICE}\" \"deviceid: ${KIA_DEVICE}\" \\\n    \"from: SPA\" \"host: api.owners.kia.com\" \"language: 0\" \\\n    \"offset: ${off}\" \"ostype: iOS\" \"osversion: 15.8.5\" \"phonebrand: iPhone\" \\\n    \"secretkey: sydnat-9kykci-Kuhtep-h5nK\" \"to: APIGW\" \"tokentype: A\" \\\n    \"date: $(LC_ALL=C date -u '+%a, %d %b %Y %H:%M:%S GMT')\" \\\n    \"user-agent: KIAPrimo_iOS/37 CFNetwork/1335.0.3.4 Darwin/21.6.0\" \\\n    \"$@\"\n  do\n    KIA_HDRS+=(-H \"$h\")\n  done\n}\n\n# curl wrapper: kia_curl <method> <path> [body] [-- extra-header ...]\nkia_curl() {\n  local method=\"$1\" path=\"$2\" body=\"${3:-}\"\n  shift 2; [ $# -gt 0 ] && shift          # drop the body arg when present\n  [ \"${1:-}\" = \"--\" ] && shift\n  kia_headers \"$@\"\n  curl -sS -X \"$method\" \"${KIA_BASE}/${path}\" \"${KIA_HDRS[@]}\" \\\n    ${body:+--data \"$body\"} -D /tmp/kia_hdrs --compressed\n}\n```\n\nBoth functions work under bash and zsh. **Test under `bash` if you change them** —\nthe octal trap above bites only bash, so zsh-only testing hides it. An array is\nused rather than piping headers through `sed`, which avoids depending on GNU\nsed's `\\n`-in-replacement behaviour.\n\n`secretkey` is a **static app constant**, not a user secret — it is the same for\nevery install.\n\n## 1. Login (one-time MFA)\n\n```bash\n# Step 1 — authenticate. Captures otpKey (body) and xid (RESPONSE HEADER).\nkia_curl POST prof/authUser \"$(jq -nc \\\n  --arg u \"$KIA_USERNAME\" --arg p \"$KIA_PASSWORD\" \\\n  '{deviceKey:\"\",deviceType:2,userCredential:{userId:$u,password:$p},tncFlag:1}')\" \\\n  | tee /tmp/kia_auth.json | jq '.status, .payload.nextAction'\n\nOTPKEY=$(jq -r '.payload.otpKey' /tmp/kia_auth.json)\nXID=$(grep -i '^xid:' /tmp/kia_hdrs | tr -d '\\r' | cut -d' ' -f2)\n```\n\n> Stop here if `status.errorCode` is `1001` or `1037`. Fix the credential — do\n> **not** loop. Repeated failures set `enforceRecaptcha` and permanently break\n> shell login.\n\n```bash\n# Step 2 — send the code. notifytype is SMS or EMAIL.\nkia_curl POST cmm/sendOTP '{}' -- \\\n  \"otpkey: $OTPKEY\" \"notifytype: SMS\" \"xid: $XID\" | jq '.status, .payload.message'\n\n# Step 3 — verify. sid + rmtoken come back as RESPONSE HEADERS.\nread -r -p 'code: ' CODE\nkia_curl POST cmm/verifyOTP \"$(jq -nc --arg o \"$CODE\" '{otp:$o}')\" -- \\\n  \"otpkey: $OTPKEY\" \"xid: $XID\" | jq '.status'\n\nSID=$(grep -i '^sid:'     /tmp/kia_hdrs | tr -d '\\r' | cut -d' ' -f2)\nRMTOKEN=$(grep -i '^rmtoken:' /tmp/kia_hdrs | tr -d '\\r' | cut -d' ' -f2)\n\nmkdir -p \"$(dirname \"$KIA_SESSION\")\" && chmod 700 \"$(dirname \"$KIA_SESSION\")\"\njq -nc --arg r \"$RMTOKEN\" --arg d \"$KIA_DEVICE\" '{rmtoken:$r,deviceId:$d}' \\\n  > \"$KIA_SESSION\"\nchmod 600 \"$KIA_SESSION\"\n```\n\n`$KIA_SESSION` — **not** `session.json`. That neighbouring file is the MCP\nserver's own store (keyed by `accountId`, different schema); clobbering it sends\nthe server back through MFA.\n\n## 2. Refresh — no MFA, use this every other time\n\n```bash\nRMTOKEN=$(jq -r .rmtoken \"$KIA_SESSION\")\nKIA_DEVICE=$(jq -r .deviceId \"$KIA_SESSION\")\n\nkia_curl POST prof/authUser \"$(jq -nc \\\n  --arg u \"$KIA_USERNAME\" --arg p \"$KIA_PASSWORD\" --arg d \"$KIA_DEVICE\" \\\n  '{deviceKey:$d,deviceType:2,userCredential:{userId:$u,password:$p}}')\" \\\n  -- \"rmtoken: $RMTOKEN\" | jq '.status.statusCode'\n\nSID=$(grep -i '^sid:' /tmp/kia_hdrs | tr -d '\\r' | cut -d' ' -f2)\n```\n\nThe `rmtoken` is **not** rotated — the stored one keeps working.\n\n## 3. Reads\n\n```bash\n# Vehicle list -> vinkey\nkia_curl GET ownr/gvl '' -- \"sid: $SID\" | tee /tmp/kia_gvl.json \\\n  | jq '.payload.vehicleSummary[] | {nickName, modelYear, modelName, mileage, vehicleKey}'\nVIN=$(jq -r '.payload.vehicleSummary[0].vehicleKey' /tmp/kia_gvl.json)\n```\n\n```bash\n# Cached status. airTempRange/seatHeatCoolOption = \"1\" or the climate block is ABSENT.\nGVI=$(jq -nc --arg v \"$VIN\" '{\n  vehicleConfigReq:{airTempRange:\"1\",maintenance:\"1\",seatHeatCoolOption:\"1\",\n                    vehicle:\"1\",vehicleFeature:\"1\"},\n  vehicleInfoReq:{drivingActivty:\"0\",dtc:\"1\",enrollment:\"1\",functionalCards:\"0\",\n                  location:\"1\",vehicleStatus:\"1\",weather:\"0\"},\n  vinKey:[$v]}')\n\nkia_curl POST cmm/gvi \"$GVI\" -- \"sid: $SID\" \"vinkey: $VIN\" > /tmp/kia_gvi.json\n\n# The fields worth looking at\njq '.payload.vehicleInfoList[0].lastVehicleInfo.vehicleStatusRpt.vehicleStatus\n    | {doorLock, ign3, engine,\n       climate: {airCtrl: .climate.airCtrl, temp: .climate.airTemp.value},\n       battery: .evStatus.batteryStatus,\n       range:   .evStatus.drvDistance[0].rangeByFuel.totalAvailableRange.value,\n       synced:  .syncDate.utc}' /tmp/kia_gvi.json\n```\n\n`drivingActivty` is **misspelled in Kia's API**. Correcting it drops the field.\n\n```bash\n# Location\njq '.payload.vehicleInfoList[0].lastVehicleInfo.location\n    | {lat: .coord.lat, lon: .coord.lon, synced: .syncDate.utc}' /tmp/kia_gvi.json\n\n# Force a fresh read from the vehicle (slow — wakes the telematics unit)\nkia_curl POST rems/rvs '{\"requestType\":0}' -- \"sid: $SID\" \"vinkey: $VIN\" | jq '.status'\n\n# EV charge targets (AC and DC)\nkia_curl GET evc/gts '' -- \"sid: $SID\" \"vinkey: $VIN\" \\\n  | jq '.payload.targetSOClist[] | {plugType, targetSOClevel}'\n```\n\n## 4. Commands\n\n```bash\n# Snapshot the fields you intend to prove changed — NEVER include syncDate.\nkia_state() {\n  kia_curl POST cmm/gvi \"$GVI\" -- \"sid: $SID\" \"vinkey: $VIN\" \\\n    | jq -c '.payload.vehicleInfoList[0].lastVehicleInfo.vehicleStatusRpt.vehicleStatus\n             | {doorLock, ign3, airCtrl: .climate.airCtrl}'\n}\nBEFORE=$(kia_state); echo \"before: $BEFORE\"\n```\n\n```bash\n# Doors — VERIFIED\nkia_curl GET rems/door/lock   '' -- \"sid: $SID\" \"vinkey: $VIN\" | jq '.status.statusCode'\nkia_curl GET rems/door/unlock '' -- \"sid: $SID\" \"vinkey: $VIN\" | jq '.status.statusCode'\n\n# Climate on — VERIFIED. duration is minutes; temperature is best-effort (see below).\nkia_curl POST rems/start \"$(jq -nc '{remoteClimate:{\n    airTemp:{unit:1,value:\"70\"}, airCtrl:true, defrost:false,\n    heatingAccessory:{rearWindow:0,sideMirror:0,steeringWheel:0,steeringWheelStep:0},\n    ignitionOnDuration:{unit:4,value:10}}}')\" \\\n  -- \"sid: $SID\" \"vinkey: $VIN\" | jq '.status.statusCode'\n\n# Climate off — VERIFIED\nkia_curl GET rems/stop '' -- \"sid: $SID\" \"vinkey: $VIN\" | jq '.status.statusCode'\n```\n\nOmit `heatVentSeat` unless you know the car supports the seats you name — Kia\nvalidates seat capability per vehicle.\n\n```bash\n# Charging — VERIFIED against a plugged-in EV9.\n# Proof: vehicleStatus.evStatus.batteryCharge flips; evc/sts proven via evc/gts.\nkia_curl POST evc/charge '{\"chargeRatio\":100}' -- \"sid: $SID\" \"vinkey: $VIN\"\nkia_curl GET  evc/cancel ''                    -- \"sid: $SID\" \"vinkey: $VIN\"\n\n# Send BOTH plug types — evc/sts replaces the list, so omitting one drops it.\nkia_curl POST evc/sts '{\"targetSOClist\":[{\"plugType\":0,\"targetSOClevel\":90},\n                                          {\"plugType\":1,\"targetSOClevel\":80}]}' \\\n  -- \"sid: $SID\" \"vinkey: $VIN\"\n\n# Charging state, for proving the above\nkia_curl POST cmm/gvi \"$GVI\" -- \"sid: $SID\" \"vinkey: $VIN\" \\\n  | jq '.payload.vehicleInfoList[0].lastVehicleInfo.vehicleStatusRpt.vehicleStatus.evStatus\n        | {batteryCharge, batteryStatus, batteryPlugin}'\n```\n\n## 5. Prove it landed\n\n```bash\n# Poll the re-read. This — not cmm/gts — is the proof.\nfor i in $(seq 1 9); do\n  sleep 10\n  AFTER=$(kia_state)\n  [ \"$AFTER\" != \"$BEFORE\" ] && { echo \"changed: $BEFORE -> $AFTER\"; break; }\n  [ \"$i\" = 9 ] && echo \"NO observed change in ~90s: $AFTER\"\ndone\n```\n\n`plugType` 0 and 1 are the two charge connectors; `targetSOClevel` is a percentage.\n\n## Error quick-reference\n\n| errorCode | Meaning | Do |\n| --- | --- | --- |\n| `0` | success | — |\n| `9200` | missing mandatory header | regenerate `date`; it must be fresh RFC-1123 |\n| `1001` | invalid email or password | fix it; **do not retry in a loop** |\n| `1037` | invalid email address | check the address format/domain |\n\nSession expired (a previously-working `sid` starts failing) → re-run §2. It needs\nno MFA.\n\nFile v2.0.0:skill-card.md\n\n## Description:\n\nGuides one-off Kia vehicle status checks and remote commands through shell requests to the Kia Owners API.\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\nKia owners and their authorized assistants use the skill to inspect vehicle status, location, and charging information or request door, climate, and charging actions from the shell.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: The skill can access sensitive account and vehicle location data and issue real vehicle commands.\n\nMitigation: Run only on a private machine and require explicit confirmation before unlock, climate, or charging actions.\n\nRisk: Durable session credentials and temporary response files can expose account or vehicle data.\n\nMitigation: Protect the saved session file, restrict access to temporary files, and delete them when finished.\n\n## Reference(s):\n\n- [ClawHub skill release](https://clawhub.ai/chrischall/skills/kiaaccess-curl)\n- [Ready-to-run requests](references/requests.md)\n\n## Skill Output:\n\n**Output Type(s):** [Guidance, Shell commands]\n\n**Output Format:** [Markdown with bash examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Requires account credentials and one-time MFA setup; command acceptance does not prove a vehicle state change.]\n\n## Skill Version(s):\n\n2.0.0 (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.1.5: 4 files, 8488 bytes\n\nFiles: references/requests.md (9510b), skill-card.md (2331b), SKILL.md (4737b), _meta.json (133b)\n\nFile v1.1.5:SKILL.md\n\n---\nname: kiaaccess-curl\ndescription: Query and command a Kia vehicle directly with curl against the Kia Owners API (api.owners.kia.com), without running the MCP server. Use when the user wants a one-off read of their Kia's status, location, or EV charge state, or to lock/unlock/start climate from the shell — \"check my Kia\", \"is the car locked\", \"what's the EV9 charge\", \"lock the car from the terminal\". Requires KIA_USERNAME/KIA_PASSWORD and a one-time SMS/email MFA bootstrap.\n---\n\n# Kia Owners API via curl\n\nThe Kia Access app's API is reachable server-side — no browser, no bridge, no\nextension. This skill talks to it directly with `curl`.\n\nPrefer the `kiaaccess-mcp` server for anything conversational or repeated; use\nthis for one-off shell work, debugging, or when the server isn't running.\n\n**Ready-to-run request bodies and `jq` recipes: `references/requests.md`.**\nFull shape reference (verified live): `../../docs/KIA-API.md`.\n\n## Setup\n\n```bash\nexport KIA_USERNAME='you@example.com'\nexport KIA_PASSWORD='…'\nexport KIA_DEVICE=$(uuidgen)        # keep this stable across runs\n```\n\n## Two rules that will bite you\n\n1. **Every request needs an RFC-1123 `date` header.** Omit it and you get\n   `errorCode 9200 \"Missing mandatory data in header\"` — a message that does not\n   name the culprit. Regenerate it per request; a stale one is rejected.\n2. **HTTP is 200 even on failure.** Success is `status.statusCode == 0` in the\n   *body*. Never branch on the HTTP code.\n\nSource `references/requests.md`'s `kia_headers` helper rather than hand-rolling\nheaders — it handles both.\n\n## Auth: one-time MFA, then silent refresh\n\n`authUser` → `sendOTP` → `verifyOTP` yields a **`sid`** (session, short-lived)\nand an **`rmtoken`** (refresh, durable), both as *response headers*.\n\nAfterwards, `authUser` with the `rmtoken` header mints a fresh `sid` with **no\nMFA**. So you do the SMS dance once and then never again — save the `rmtoken`.\n\n> **Never retry a rejected login.** `errorCode 1001` (bad credentials) or `1037`\n> (bad email) increments `payload.loginAttempt`; enough failures set\n> `enforceRecaptcha` and **permanently break shell-based login**. Fix the\n> credential and try once.\n\nStore the `rmtoken` at `$KIA_SESSION` (default `~/.kiaaccess-mcp/curl-session.json`)\nwith `chmod 600`. It is a credential: it re-authenticates the account without a\npassword prompt.\n\n> Do **not** write it to `~/.kiaaccess-mcp/session.json`. That path belongs to the\n> `kiaaccess-mcp` server, whose store is keyed by `accountId` with a different\n> schema — overwriting it corrupts the server's session and forces it back\n> through MFA.\n\n## Calling\n\nReads and commands take `sid` (+ `vinkey` for vehicle-scoped calls). Get the\n`vinkey` from `ownr/gvl` → `payload.vehicleSummary[0].vehicleKey`.\n\n| Want | Endpoint |\n| --- | --- |\n| vehicles | `GET ownr/gvl` |\n| status (cached) | `POST cmm/gvi` |\n| status (force refresh) | `POST rems/rvs` |\n| EV charge targets | `GET evc/gts` |\n| lock / unlock | `GET rems/door/lock` / `rems/door/unlock` |\n| climate on / off | `POST rems/start` / `GET rems/stop` |\n| charge start / stop / limits | `POST evc/charge` / `GET evc/cancel` / `POST evc/sts` |\n\n## Confirming a command actually worked\n\n**A `statusCode: 0` means \"accepted\", not \"done\".**\n\n- **Do not poll `cmm/gts`.** Despite taking an `xid`, it returns global flags and\n  never reports per-action completion — polled through a real lock it never\n  changed.\n- **Re-read `cmm/gvi` and diff the field.** That is the only proof. Allow ~30–60s.\n\nFields to diff:\n\n| Command | Field |\n| --- | --- |\n| lock / unlock | `vehicleStatus.doorLock` |\n| climate on / off | `vehicleStatus.climate.airCtrl` and `vehicleStatus.ign3` |\n\nThree traps when writing that comparison:\n\n- **`syncDate` advances on every read.** Include it and *every* command looks\n  successful. Exclude it.\n- **There is no `airCtrlOn`.** Climate is nested under `vehicleStatus.climate`,\n  and the whole block is **absent** unless you request `cmm/gvi` with\n  `vehicleConfigReq.airTempRange: \"1\"` and `seatHeatCoolOption: \"1\"`.\n- **On an EV, `engine` stays `false`** with climate running — use `ign3`.\n\n## Verification status\n\nEvery endpoint here was verified live against a 2024 EV9 — all reads, the door\nand climate commands, and (against a plugged-in car) `evc/charge`, `evc/cancel`\nand `evc/sts`, each proven by a re-read rather than a 200.\n\nCharging proof fields live under `vehicleStatus.evStatus`: `batteryCharge`\n(true while charging), `batteryStatus` (SOC %), `batteryPlugin`. `evc/sts` is\nproven by re-reading `evc/gts`.\n\nOne caveat: **`rems/start`'s temperature may not apply** — a start requesting 70\nleft `airTemp.value` at 72. Treat it as best-effort.\n\nFile v1.1.5:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"kiaaccess-curl\",\n  \"version\": \"1.1.5\",\n  \"publishedAt\": 1791380127254\n}\n\nFile v1.1.5:references/requests.md\n\n# Ready-to-run requests\n\nEvery shape here was verified live on 2026-07-27 against a 2024 Kia EV9 —\nincluding the `evc/*` charging commands, run against the car while plugged in.\n\n## Header helper\n\nSource this first. It regenerates the mandatory `date` per call and keeps the\ndevice id stable.\n\n```bash\nKIA_BASE='https://api.owners.kia.com/apigw/v1'\n: \"${KIA_DEVICE:?export KIA_DEVICE=\\$(uuidgen) first}\"\n\n# Session file for THIS skill. Deliberately NOT ~/.kiaaccess-mcp/session.json —\n# that path belongs to the MCP server, whose store is keyed by accountId with a\n# different schema (src/session.ts). Writing this skill's flat\n# {rmtoken, deviceId} there corrupts the server's session and forces it back\n# through MFA.\nKIA_SESSION=\"${KIA_CURL_SESSION:-$HOME/.kiaaccess-mcp/curl-session.json}\"\n\n# Builds the header list into the KIA_HDRS array. Extra headers passed as args.\nkia_headers() {\n  local z sign off h\n  z=$(date +%z)                       # e.g. -0800, +0530\n  sign=${z:0:1}\n  # `10#` forces base 10. Without it, bash reads a leading-zero offset such as\n  # `08`/`09` as OCTAL and dies with \"value too great for base\" — so this breaks\n  # in US Pacific winter, Alaska, Japan, Korea and China. zsh does not have the\n  # problem, which is a good way to ship it broken without noticing.\n  off=$(( 10#${z:1:2} ))\n  [ \"$sign\" = \"-\" ] && off=$(( 0 - off ))\n\n  KIA_HDRS=()\n  for h in \\\n    \"content-type: application/json;charset=utf-8\" \\\n    \"accept: application/json\" \\\n    \"accept-language: en-US,en;q=0.9\" \\\n    \"accept-charset: utf-8\" \\\n    \"apptype: L\" \"appversion: 7.22.0\" \"clientid: SPACL716-APL\" \\\n    \"clientuuid: ${KIA_DEVICE}\" \"deviceid: ${KIA_DEVICE}\" \\\n    \"from: SPA\" \"host: api.owners.kia.com\" \"language: 0\" \\\n    \"offset: ${off}\" \"ostype: iOS\" \"osversion: 15.8.5\" \"phonebrand: iPhone\" \\\n    \"secretkey: sydnat-9kykci-Kuhtep-h5nK\" \"to: APIGW\" \"tokentype: A\" \\\n    \"date: $(LC_ALL=C date -u '+%a, %d %b %Y %H:%M:%S GMT')\" \\\n    \"user-agent: KIAPrimo_iOS/37 CFNetwork/1335.0.3.4 Darwin/21.6.0\" \\\n    \"$@\"\n  do\n    KIA_HDRS+=(-H \"$h\")\n  done\n}\n\n# curl wrapper: kia_curl <method> <path> [body] [-- extra-header ...]\nkia_curl() {\n  local method=\"$1\" path=\"$2\" body=\"${3:-}\"\n  shift 2; [ $# -gt 0 ] && shift          # drop the body arg when present\n  [ \"${1:-}\" = \"--\" ] && shift\n  kia_headers \"$@\"\n  curl -sS -X \"$method\" \"${KIA_BASE}/${path}\" \"${KIA_HDRS[@]}\" \\\n    ${body:+--data \"$body\"} -D /tmp/kia_hdrs --compressed\n}\n```\n\nBoth functions work under bash and zsh. **Test under `bash` if you change them** —\nthe octal trap above bites only bash, so zsh-only testing hides it. An array is\nused rather than piping headers through `sed`, which avoids depending on GNU\nsed's `\\n`-in-replacement behaviour.\n\n`secretkey` is a **static app constant**, not a user secret — it is the same for\nevery install.\n\n## 1. Login (one-time MFA)\n\n```bash\n# Step 1 — authenticate. Captures otpKey (body) and xid (RESPONSE HEADER).\nkia_curl POST prof/authUser \"$(jq -nc \\\n  --arg u \"$KIA_USERNAME\" --arg p \"$KIA_PASSWORD\" \\\n  '{deviceKey:\"\",deviceType:2,userCredential:{userId:$u,password:$p},tncFlag:1}')\" \\\n  | tee /tmp/kia_auth.json | jq '.status, .payload.nextAction'\n\nOTPKEY=$(jq -r '.payload.otpKey' /tmp/kia_auth.json)\nXID=$(grep -i '^xid:' /tmp/kia_hdrs | tr -d '\\r' | cut -d' ' -f2)\n```\n\n> Stop here if `status.errorCode` is `1001` or `1037`. Fix the credential — do\n> **not** loop. Repeated failures set `enforceRecaptcha` and permanently break\n> shell login.\n\n```bash\n# Step 2 — send the code. notifytype is SMS or EMAIL.\nkia_curl POST cmm/sendOTP '{}' -- \\\n  \"otpkey: $OTPKEY\" \"notifytype: SMS\" \"xid: $XID\" | jq '.status, .payload.message'\n\n# Step 3 — verify. sid + rmtoken come back as RESPONSE HEADERS.\nread -r -p 'code: ' CODE\nkia_curl POST cmm/verifyOTP \"$(jq -nc --arg o \"$CODE\" '{otp:$o}')\" -- \\\n  \"otpkey: $OTPKEY\" \"xid: $XID\" | jq '.status'\n\nSID=$(grep -i '^sid:'     /tmp/kia_hdrs | tr -d '\\r' | cut -d' ' -f2)\nRMTOKEN=$(grep -i '^rmtoken:' /tmp/kia_hdrs | tr -d '\\r' | cut -d' ' -f2)\n\nmkdir -p \"$(dirname \"$KIA_SESSION\")\" && chmod 700 \"$(dirname \"$KIA_SESSION\")\"\njq -nc --arg r \"$RMTOKEN\" --arg d \"$KIA_DEVICE\" '{rmtoken:$r,deviceId:$d}' \\\n  > \"$KIA_SESSION\"\nchmod 600 \"$KIA_SESSION\"\n```\n\n`$KIA_SESSION` — **not** `session.json`. That neighbouring file is the MCP\nserver's own store (keyed by `accountId`, different schema); clobbering it sends\nthe server back through MFA.\n\n## 2. Refresh — no MFA, use this every other time\n\n```bash\nRMTOKEN=$(jq -r .rmtoken \"$KIA_SESSION\")\nKIA_DEVICE=$(jq -r .deviceId \"$KIA_SESSION\")\n\nkia_curl POST prof/authUser \"$(jq -nc \\\n  --arg u \"$KIA_USERNAME\" --arg p \"$KIA_PASSWORD\" --arg d \"$KIA_DEVICE\" \\\n  '{deviceKey:$d,deviceType:2,userCredential:{userId:$u,password:$p}}')\" \\\n  -- \"rmtoken: $RMTOKEN\" | jq '.status.statusCode'\n\nSID=$(grep -i '^sid:' /tmp/kia_hdrs | tr -d '\\r' | cut -d' ' -f2)\n```\n\nThe `rmtoken` is **not** rotated — the stored one keeps working.\n\n## 3. Reads\n\n```bash\n# Vehicle list -> vinkey\nkia_curl GET ownr/gvl '' -- \"sid: $SID\" | tee /tmp/kia_gvl.json \\\n  | jq '.payload.vehicleSummary[] | {nickName, modelYear, modelName, mileage, vehicleKey}'\nVIN=$(jq -r '.payload.vehicleSummary[0].vehicleKey' /tmp/kia_gvl.json)\n```\n\n```bash\n# Cached status. airTempRange/seatHeatCoolOption = \"1\" or the climate block is ABSENT.\nGVI=$(jq -nc --arg v \"$VIN\" '{\n  vehicleConfigReq:{airTempRange:\"1\",maintenance:\"1\",seatHeatCoolOption:\"1\",\n                    vehicle:\"1\",vehicleFeature:\"1\"},\n  vehicleInfoReq:{drivingActivty:\"0\",dtc:\"1\",enrollment:\"1\",functionalCards:\"0\",\n                  location:\"1\",vehicleStatus:\"1\",weather:\"0\"},\n  vinKey:[$v]}')\n\nkia_curl POST cmm/gvi \"$GVI\" -- \"sid: $SID\" \"vinkey: $VIN\" > /tmp/kia_gvi.json\n\n# The fields worth looking at\njq '.payload.vehicleInfoList[0].lastVehicleInfo.vehicleStatusRpt.vehicleStatus\n    | {doorLock, ign3, engine,\n       climate: {airCtrl: .climate.airCtrl, temp: .climate.airTemp.value},\n       battery: .evStatus.batteryStatus,\n       range:   .evStatus.drvDistance[0].rangeByFuel.totalAvailableRange.value,\n       synced:  .syncDate.utc}' /tmp/kia_gvi.json\n```\n\n`drivingActivty` is **misspelled in Kia's API**. Correcting it drops the field.\n\n```bash\n# Location\njq '.payload.vehicleInfoList[0].lastVehicleInfo.location\n    | {lat: .coord.lat, lon: .coord.lon, synced: .syncDate.utc}' /tmp/kia_gvi.json\n\n# Force a fresh read from the vehicle (slow — wakes the telematics unit)\nkia_curl POST rems/rvs '{\"requestType\":0}' -- \"sid: $SID\" \"vinkey: $VIN\" | jq '.status'\n\n# EV charge targets (AC and DC)\nkia_curl GET evc/gts '' -- \"sid: $SID\" \"vinkey: $VIN\" \\\n  | jq '.payload.targetSOClist[] | {plugType, targetSOClevel}'\n```\n\n## 4. Commands\n\n```bash\n# Snapshot the fields you intend to prove changed — NEVER include syncDate.\nkia_state() {\n  kia_curl POST cmm/gvi \"$GVI\" -- \"sid: $SID\" \"vinkey: $VIN\" \\\n    | jq -c '.payload.vehicleInfoList[0].lastVehicleInfo.vehicleStatusRpt.vehicleStatus\n             | {doorLock, ign3, airCtrl: .climate.airCtrl}'\n}\nBEFORE=$(kia_state); echo \"before: $BEFORE\"\n```\n\n```bash\n# Doors — VERIFIED\nkia_curl GET rems/door/lock   '' -- \"sid: $SID\" \"vinkey: $VIN\" | jq '.status.statusCode'\nkia_curl GET rems/door/unlock '' -- \"sid: $SID\" \"vinkey: $VIN\" | jq '.status.statusCode'\n\n# Climate on — VERIFIED. duration is minutes; temperature is best-effort (see below).\nkia_curl POST rems/start \"$(jq -nc '{remoteClimate:{\n    airTemp:{unit:1,value:\"70\"}, airCtrl:true, defrost:false,\n    heatingAccessory:{rearWindow:0,sideMirror:0,steeringWheel:0,steeringWheelStep:0},\n    ignitionOnDuration:{unit:4,value:10}}}')\" \\\n  -- \"sid: $SID\" \"vinkey: $VIN\" | jq '.status.statusCode'\n\n# Climate off — VERIFIED\nkia_curl GET rems/stop '' -- \"sid: $SID\" \"vinkey: $VIN\" | jq '.status.statusCode'\n```\n\nOmit `heatVentSeat` unless you know the car supports the seats you name — Kia\nvalidates seat capability per vehicle.\n\n```bash\n# Charging — VERIFIED against a plugged-in EV9.\n# Proof: vehicleStatus.evStatus.batteryCharge flips; evc/sts proven via evc/gts.\nkia_curl POST evc/charge '{\"chargeRatio\":100}' -- \"sid: $SID\" \"vinkey: $VIN\"\nkia_curl GET  evc/cancel ''                    -- \"sid: $SID\" \"vinkey: $VIN\"\n\n# Send BOTH plug types — evc/sts replaces the list, so omitting one drops it.\nkia_curl POST evc/sts '{\"targetSOClist\":[{\"plugType\":0,\"targetSOClevel\":90},\n                                          {\"plugType\":1,\"targetSOClevel\":80}]}' \\\n  -- \"sid: $SID\" \"vinkey: $VIN\"\n\n# Charging state, for proving the above\nkia_curl POST cmm/gvi \"$GVI\" -- \"sid: $SID\" \"vinkey: $VIN\" \\\n  | jq '.payload.vehicleInfoList[0].lastVehicleInfo.vehicleStatusRpt.vehicleStatus.evStatus\n        | {batteryCharge, batteryStatus, batteryPlugin}'\n```\n\n## 5. Prove it landed\n\n```bash\n# Poll the re-read. This — not cmm/gts — is the proof.\nfor i in $(seq 1 9); do\n  sleep 10\n  AFTER=$(kia_state)\n  [ \"$AFTER\" != \"$BEFORE\" ] && { echo \"changed: $BEFORE -> $AFTER\"; break; }\n  [ \"$i\" = 9 ] && echo \"NO observed change in ~90s: $AFTER\"\ndone\n```\n\n`plugType` 0 and 1 are the two charge connectors; `targetSOClevel` is a percentage.\n\n## Error quick-reference\n\n| errorCode | Meaning | Do |\n| --- | --- | --- |\n| `0` | success | — |\n| `9200` | missing mandatory header | regenerate `date`; it must be fresh RFC-1123 |\n| `1001` | invalid email or password | fix it; **do not retry in a loop** |\n| `1037` | invalid email address | check the address format/domain |\n\nSession expired (a previously-working `sid` starts failing) → re-run §2. It needs\nno MFA.\n\nFile v1.1.5:skill-card.md\n\n## Description:\n\nGuides one-off Kia vehicle status, location, charging, and remote-control requests through shell commands to the Kia Owners API.\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\nKia owners and developers use this skill for one-off shell-based reads of vehicle status, location, and EV charging, or to issue remote door, climate, and charging commands after account authentication.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Remote commands can unlock the car, change climate or charging, and location requests expose precise whereabouts.\n\nMitigation: Require explicit user confirmation before unlock, climate, charging, or location requests; share vehicle data only with the intended user.\n\nRisk: Saved refresh tokens and temporary response files can expose Kia account or vehicle information.\n\nMitigation: Treat the refresh token as a password, store it with restricted permissions, avoid shared machines, and remove temporary files after use.\n\nRisk: An accepted remote request does not establish that the vehicle completed the action.\n\nMitigation: Reread and compare the relevant vehicle-status field before reporting success.\n\nRisk: Repeated failed logins can trigger account challenges that block shell access.\n\nMitigation: Stop after a rejected login and correct the credentials rather than retrying automatically.\n\n## Reference(s):\n\n- [KiaAccess Curl on ClawHub](https://clawhub.ai/chrischall/skills/kiaaccess-curl)\n- [Ready-to-run requests](references/requests.md)\n\n## Skill Output:\n\n**Output Type(s):** [Shell commands, Configuration instructions, Guidance]\n\n**Output Format:** [Markdown with bash and JSON examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Commands return account or vehicle data and may change vehicle state; verify command completion by rereading vehicle status.]\n\n## Skill Version(s):\n\n1.1.5 (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.1.4: 4 files, 8305 bytes\n\nFiles: references/requests.md (9510b), skill-card.md (1888b), SKILL.md (4737b), _meta.json (133b)\n\nFile v1.1.4:SKILL.md\n\n---\nname: kiaaccess-curl\ndescription: Query and command a Kia vehicle directly with curl against the Kia Owners API (api.owners.kia.com), without running the MCP server. Use when the user wants a one-off read of their Kia's status, location, or EV charge state, or to lock/unlock/start climate from the shell — \"check my Kia\", \"is the car locked\", \"what's the EV9 charge\", \"lock the car from the terminal\". Requires KIA_USERNAME/KIA_PASSWORD and a one-time SMS/email MFA bootstrap.\n---\n\n# Kia Owners API via curl\n\nThe Kia Access app's API is reachable server-side — no browser, no bridge, no\nextension. This skill talks to it directly with `curl`.\n\nPrefer the `kiaaccess-mcp` server for anything conversational or repeated; use\nthis for one-off shell work, debugging, or when the server isn't running.\n\n**Ready-to-run request bodies and `jq` recipes: `references/requests.md`.**\nFull shape reference (verified live): `../../docs/KIA-API.md`.\n\n## Setup\n\n```bash\nexport KIA_USERNAME='you@example.com'\nexport KIA_PASSWORD='…'\nexport KIA_DEVICE=$(uuidgen)        # keep this stable across runs\n```\n\n## Two rules that will bite you\n\n1. **Every request needs an RFC-1123 `date` header.** Omit it and you get\n   `errorCode 9200 \"Missing mandatory data in header\"` — a message that does not\n   name the culprit. Regenerate it per request; a stale one is rejected.\n2. **HTTP is 200 even on failure.** Success is `status.statusCode == 0` in the\n   *body*. Never branch on the HTTP code.\n\nSource `references/requests.md`'s `kia_headers` helper rather than hand-rolling\nheaders — it handles both.\n\n## Auth: one-time MFA, then silent refresh\n\n`authUser` → `sendOTP` → `verifyOTP` yields a **`sid`** (session, short-lived)\nand an **`rmtoken`** (refresh, durable), both as *response headers*.\n\nAfterwards, `authUser` with the `rmtoken` header mints a fresh `sid` with **no\nMFA**. So you do the SMS dance once and then never again — save the `rmtoken`.\n\n> **Never retry a rejected login.** `errorCode 1001` (bad credentials) or `1037`\n> (bad email) increments `payload.loginAttempt`; enough failures set\n> `enforceRecaptcha` and **permanently break shell-based login**. Fix the\n> credential and try once.\n\nStore the `rmtoken` at `$KIA_SESSION` (default `~/.kiaaccess-mcp/curl-session.json`)\nwith `chmod 600`. It is a credential: it re-authenticates the account without a\npassword prompt.\n\n> Do **not** write it to `~/.kiaaccess-mcp/session.json`. That path belongs to the\n> `kiaaccess-mcp` server, whose store is keyed by `accountId` with a different\n> schema — overwriting it corrupts the server's session and forces it back\n> through MFA.\n\n## Calling\n\nReads and commands take `sid` (+ `vinkey` for vehicle-scoped calls). Get the\n`vinkey` from `ownr/gvl` → `payload.vehicleSummary[0].vehicleKey`.\n\n| Want | Endpoint |\n| --- | --- |\n| vehicles | `GET ownr/gvl` |\n| status (cached) | `POST cmm/gvi` |\n| status (force refresh) | `POST rems/rvs` |\n| EV charge targets | `GET evc/gts` |\n| lock / unlock | `GET rems/door/lock` / `rems/door/unlock` |\n| climate on / off | `POST rems/start` / `GET rems/stop` |\n| charge start / stop / limits | `POST evc/charge` / `GET evc/cancel` / `POST evc/sts` |\n\n## Confirming a command actually worked\n\n**A `statusCode: 0` means \"accepted\", not \"done\".**\n\n- **Do not poll `cmm/gts`.** Despite taking an `xid`, it returns global flags and\n  never reports per-action completion — polled through a real lock it never\n  changed.\n- **Re-read `cmm/gvi` and diff the field.** That is the only proof. Allow ~30–60s.\n\nFields to diff:\n\n| Command | Field |\n| --- | --- |\n| lock / unlock | `vehicleStatus.doorLock` |\n| climate on / off | `vehicleStatus.climate.airCtrl` and `vehicleStatus.ign3` |\n\nThree traps when writing that comparison:\n\n- **`syncDate` advances on every read.** Include it and *every* command looks\n  successful. Exclude it.\n- **There is no `airCtrlOn`.** Climate is nested under `vehicleStatus.climate`,\n  and the whole block is **absent** unless you request `cmm/gvi` with\n  `vehicleConfigReq.airTempRange: \"1\"` and `seatHeatCoolOption: \"1\"`.\n- **On an EV, `engine` stays `false`** with climate running — use `ign3`.\n\n## Verification status\n\nEvery endpoint here was verified live against a 2024 EV9 — all reads, the door\nand climate commands, and (against a plugged-in car) `evc/charge`, `evc/cancel`\nand `evc/sts`, each proven by a re-read rather than a 200.\n\nCharging proof fields live under `vehicleStatus.evStatus`: `batteryCharge`\n(true while charging), `batteryStatus` (SOC %), `batteryPlugin`. `evc/sts` is\nproven by re-reading `evc/gts`.\n\nOne caveat: **`rems/start`'s temperature may not apply** — a start requesting 70\nleft `airTemp.value` at 72. Treat it as best-effort.\n\nFile v1.1.4:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"kiaaccess-curl\",\n  \"version\": \"1.1.4\",\n  \"publishedAt\": 1791168664042\n}\n\nFile v1.1.4:references/requests.md\n\n# Ready-to-run requests\n\nEvery shape here was verified live on 2026-07-27 against a 2024 Kia EV9 —\nincluding the `evc/*` charging commands, run against the car while plugged in.\n\n## Header helper\n\nSource this first. It regenerates the mandatory `date` per call and keeps the\ndevice id stable.\n\n```bash\nKIA_BASE='https://api.owners.kia.com/apigw/v1'\n: \"${KIA_DEVICE:?export KIA_DEVICE=\\$(uuidgen) first}\"\n\n# Session file for THIS skill. Deliberately NOT ~/.kiaaccess-mcp/session.json —\n# that path belongs to the MCP server, whose store is keyed by accountId with a\n# different schema (src/session.ts). Writing this skill's flat\n# {rmtoken, deviceId} there corrupts the server's session and forces it back\n# through MFA.\nKIA_SESSION=\"${KIA_CURL_SESSION:-$HOME/.kiaaccess-mcp/curl-session.json}\"\n\n# Builds the header list into the KIA_HDRS array. Extra headers passed as args.\nkia_headers() {\n  local z sign off h\n  z=$(date +%z)                       # e.g. -0800, +0530\n  sign=${z:0:1}\n  # `10#` forces base 10. Without it, bash reads a leading-zero offset such as\n  # `08`/`09` as OCTAL and dies with \"value too great for base\" — so this breaks\n  # in US Pacific winter, Alaska, Japan, Korea and China. zsh does not have the\n  # problem, which is a good way to ship it broken without noticing.\n  off=$(( 10#${z:1:2} ))\n  [ \"$sign\" = \"-\" ] && off=$(( 0 - off ))\n\n  KIA_HDRS=()\n  for h in \\\n    \"content-type: application/json;charset=utf-8\" \\\n    \"accept: application/json\" \\\n    \"accept-language: en-US,en;q=0.9\" \\\n    \"accept-charset: utf-8\" \\\n    \"apptype: L\" \"appversion: 7.22.0\" \"clientid: SPACL716-APL\" \\\n    \"clientuuid: ${KIA_DEVICE}\" \"deviceid: ${KIA_DEVICE}\" \\\n    \"from: SPA\" \"host: api.owners.kia.com\" \"language: 0\" \\\n    \"offset: ${off}\" \"ostype: iOS\" \"osversion: 15.8.5\" \"phonebrand: iPhone\" \\\n    \"secretkey: sydnat-9kykci-Kuhtep-h5nK\" \"to: APIGW\" \"tokentype: A\" \\\n    \"date: $(LC_ALL=C date -u '+%a, %d %b %Y %H:%M:%S GMT')\" \\\n    \"user-agent: KIAPrimo_iOS/37 CFNetwork/1335.0.3.4 Darwin/21.6.0\" \\\n    \"$@\"\n  do\n    KIA_HDRS+=(-H \"$h\")\n  done\n}\n\n# curl wrapper: kia_curl <method> <path> [body] [-- extra-header ...]\nkia_curl() {\n  local method=\"$1\" path=\"$2\" body=\"${3:-}\"\n  shift 2; [ $# -gt 0 ] && shift          # drop the body arg when present\n  [ \"${1:-}\" = \"--\" ] && shift\n  kia_headers \"$@\"\n  curl -sS -X \"$method\" \"${KIA_BASE}/${path}\" \"${KIA_HDRS[@]}\" \\\n    ${body:+--data \"$body\"} -D /tmp/kia_hdrs --compressed\n}\n```\n\nBoth functions work under bash and zsh. **Test under `bash` if you change them** —\nthe octal trap above bites only bash, so zsh-only testing hides it. An array is\nused rather than piping headers through `sed`, which avoids depending on GNU\nsed's `\\n`-in-replacement behaviour.\n\n`secretkey` is a **static app constant**, not a user secret — it is the same for\nevery install.\n\n## 1. Login (one-time MFA)\n\n```bash\n# Step 1 — authenticate. Captures otpKey (body) and xid (RESPONSE HEADER).\nkia_curl POST prof/authUser \"$(jq -nc \\\n  --arg u \"$KIA_USERNAME\" --arg p \"$KIA_PASSWORD\" \\\n  '{deviceKey:\"\",deviceType:2,userCredential:{userId:$u,password:$p},tncFlag:1}')\" \\\n  | tee /tmp/kia_auth.json | jq '.status, .payload.nextAction'\n\nOTPKEY=$(jq -r '.payload.otpKey' /tmp/kia_auth.json)\nXID=$(grep -i '^xid:' /tmp/kia_hdrs | tr -d '\\r' | cut -d' ' -f2)\n```\n\n> Stop here if `status.errorCode` is `1001` or `1037`. Fix the credential — do\n> **not** loop. Repeated failures set `enforceRecaptcha` and permanently break\n> shell login.\n\n```bash\n# Step 2 — send the code. notifytype is SMS or EMAIL.\nkia_curl POST cmm/sendOTP '{}' -- \\\n  \"otpkey: $OTPKEY\" \"notifytype: SMS\" \"xid: $XID\" | jq '.status, .payload.message'\n\n# Step 3 — verify. sid + rmtoken come back as RESPONSE HEADERS.\nread -r -p 'code: ' CODE\nkia_curl POST cmm/verifyOTP \"$(jq -nc --arg o \"$CODE\" '{otp:$o}')\" -- \\\n  \"otpkey: $OTPKEY\" \"xid: $XID\" | jq '.status'\n\nSID=$(grep -i '^sid:'     /tmp/kia_hdrs | tr -d '\\r' | cut -d' ' -f2)\nRMTOKEN=$(grep -i '^rmtoken:' /tmp/kia_hdrs | tr -d '\\r' | cut -d' ' -f2)\n\nmkdir -p \"$(dirname \"$KIA_SESSION\")\" && chmod 700 \"$(dirname \"$KIA_SESSION\")\"\njq -nc --arg r \"$RMTOKEN\" --arg d \"$KIA_DEVICE\" '{rmtoken:$r,deviceId:$d}' \\\n  > \"$KIA_SESSION\"\nchmod 600 \"$KIA_SESSION\"\n```\n\n`$KIA_SESSION` — **not** `session.json`. That neighbouring file is the MCP\nserver's own store (keyed by `accountId`, different schema); clobbering it sends\nthe server back through MFA.\n\n## 2. Refresh — no MFA, use this every other time\n\n```bash\nRMTOKEN=$(jq -r .rmtoken \"$KIA_SESSION\")\nKIA_DEVICE=$(jq -r .deviceId \"$KIA_SESSION\")\n\nkia_curl POST prof/authUser \"$(jq -nc \\\n  --arg u \"$KIA_USERNAME\" --arg p \"$KIA_PASSWORD\" --arg d \"$KIA_DEVICE\" \\\n  '{deviceKey:$d,deviceType:2,userCredential:{userId:$u,password:$p}}')\" \\\n  -- \"rmtoken: $RMTOKEN\" | jq '.status.statusCode'\n\nSID=$(grep -i '^sid:' /tmp/kia_hdrs | tr -d '\\r' | cut -d' ' -f2)\n```\n\nThe `rmtoken` is **not** rotated — the stored one keeps working.\n\n## 3. Reads\n\n```bash\n# Vehicle list -> vinkey\nkia_curl GET ownr/gvl '' -- \"sid: $SID\" | tee /tmp/kia_gvl.json \\\n  | jq '.payload.vehicleSummary[] | {nickName, modelYear, modelName, mileage, vehicleKey}'\nVIN=$(jq -r '.payload.vehicleSummary[0].vehicleKey' /tmp/kia_gvl.json)\n```\n\n```bash\n# Cached status. airTempRange/seatHeatCoolOption = \"1\" or the climate block is ABSENT.\nGVI=$(jq -nc --arg v \"$VIN\" '{\n  vehicleConfigReq:{airTempRange:\"1\",maintenance:\"1\",seatHeatCoolOption:\"1\",\n                    vehicle:\"1\",vehicleFeature:\"1\"},\n  vehicleInfoReq:{drivingActivty:\"0\",dtc:\"1\",enrollment:\"1\",functionalCards:\"0\",\n                  location:\"1\",vehicleStatus:\"1\",weather:\"0\"},\n  vinKey:[$v]}')\n\nkia_curl POST cmm/gvi \"$GVI\" -- \"sid: $SID\" \"vinkey: $VIN\" > /tmp/kia_gvi.json\n\n# The fields worth looking at\njq '.payload.vehicleInfoList[0].lastVehicleInfo.vehicleStatusRpt.vehicleStatus\n    | {doorLock, ign3, engine,\n       climate: {airCtrl: .climate.airCtrl, temp: .climate.airTemp.value},\n       battery: .evStatus.batteryStatus,\n       range:   .evStatus.drvDistance[0].rangeByFuel.totalAvailableRange.value,\n       synced:  .syncDate.utc}' /tmp/kia_gvi.json\n```\n\n`drivingActivty` is **misspelled in Kia's API**. Correcting it drops the field.\n\n```bash\n# Location\njq '.payload.vehicleInfoList[0].lastVehicleInfo.location\n    | {lat: .coord.lat, lon: .coord.lon, synced: .syncDate.utc}' /tmp/kia_gvi.json\n\n# Force a fresh read from the vehicle (slow — wakes the telematics unit)\nkia_curl POST rems/rvs '{\"requestType\":0}' -- \"sid: $SID\" \"vinkey: $VIN\" | jq '.status'\n\n# EV charge targets (AC and DC)\nkia_curl GET evc/gts '' -- \"sid: $SID\" \"vinkey: $VIN\" \\\n  | jq '.payload.targetSOClist[] | {plugType, targetSOClevel}'\n```\n\n## 4. Commands\n\n```bash\n# Snapshot the fields you intend to prove changed — NEVER include syncDate.\nkia_state() {\n  kia_curl POST cmm/gvi \"$GVI\" -- \"sid: $SID\" \"vinkey: $VIN\" \\\n    | jq -c '.payload.vehicleInfoList[0].lastVehicleInfo.vehicleStatusRpt.vehicleStatus\n             | {doorLock, ign3, airCtrl: .climate.airCtrl}'\n}\nBEFORE=$(kia_state); echo \"before: $BEFORE\"\n```\n\n```bash\n# Doors — VERIFIED\nkia_curl GET rems/door/lock   '' -- \"sid: $SID\" \"vinkey: $VIN\" | jq '.status.statusCode'\nkia_curl GET rems/door/unlock '' -- \"sid: $SID\" \"vinkey: $VIN\" | jq '.status.statusCode'\n\n# Climate on — VERIFIED. duration is minutes; temperature is best-effort (see below).\nkia_curl POST rems/start \"$(jq -nc '{remoteClimate:{\n    airTemp:{unit:1,value:\"70\"}, airCtrl:true, defrost:false,\n    heatingAccessory:{rearWindow:0,sideMirror:0,steeringWheel:0,steeringWheelStep:0},\n    ignitionOnDuration:{unit:4,value:10}}}')\" \\\n  -- \"sid: $SID\" \"vinkey: $VIN\" | jq '.status.statusCode'\n\n# Climate off — VERIFIED\nkia_curl GET rems/stop '' -- \"sid: $SID\" \"vinkey: $VIN\" | jq '.status.statusCode'\n```\n\nOmit `heatVentSeat` unless you know the car supports the seats you name — Kia\nvalidates seat capability per vehicle.\n\n```bash\n# Charging — VERIFIED against a plugged-in EV9.\n# Proof: vehicleStatus.evStatus.batteryCharge flips; evc/sts proven via evc/gts.\nkia_curl POST evc/charge '{\"chargeRatio\":100}' -- \"sid: $SID\" \"vinkey: $VIN\"\nkia_curl GET  evc/cancel ''                    -- \"sid: $SID\" \"vinkey: $VIN\"\n\n# Send BOTH plug types — evc/sts replaces the list, so omitting one drops it.\nkia_curl POST evc/sts '{\"targetSOClist\":[{\"plugType\":0,\"targetSOClevel\":90},\n                                          {\"plugType\":1,\"targetSOClevel\":80}]}' \\\n  -- \"sid: $SID\" \"vinkey: $VIN\"\n\n# Charging state, for proving the above\nkia_curl POST cmm/gvi \"$GVI\" -- \"sid: $SID\" \"vinkey: $VIN\" \\\n  | jq '.payload.vehicleInfoList[0].lastVehicleInfo.vehicleStatusRpt.vehicleStatus.evStatus\n        | {batteryCharge, batteryStatus, batteryPlugin}'\n```\n\n## 5. Prove it landed\n\n```bash\n# Poll the re-read. This — not cmm/gts — is the proof.\nfor i in $(seq 1 9); do\n  sleep 10\n  AFTER=$(kia_state)\n  [ \"$AFTER\" != \"$BEFORE\" ] && { echo \"changed: $BEFORE -> $AFTER\"; break; }\n  [ \"$i\" = 9 ] && echo \"NO observed change in ~90s: $AFTER\"\ndone\n```\n\n`plugType` 0 and 1 are the two charge connectors; `targetSOClevel` is a percentage.\n\n## Error quick-reference\n\n| errorCode | Meaning | Do |\n| --- | --- | --- |\n| `0` | success | — |\n| `9200` | missing mandatory header | regenerate `date`; it must be fresh RFC-1123 |\n| `1001` | invalid email or password | fix it; **do not retry in a loop** |\n| `1037` | invalid email address | check the address format/domain |\n\nSession expired (a previously-working `sid` starts failing) → re-run §2. It needs\nno MFA.\n\nFile v1.1.4:skill-card.md\n\n## Description:\n\nGuides one-off Kia vehicle status checks and remote commands through shell requests to the Kia Owners API.\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\nKia owners and developers use this skill for one-off shell-based vehicle status, location, and EV charging checks, or to request door, climate, and charging actions on a vehicle they control. It requires account credentials and an initial SMS or email verification step.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Remote commands can change vehicle door locks, climate, or charging state.\n\nMitigation: Use only for a vehicle and account you control, and obtain explicit confirmation before each command.\n\nRisk: The example workflow writes reusable account credentials to predictable temporary files.\n\nMitigation: Run only on a trusted single-user machine; keep session credentials private and avoid running the example workflow as-is on shared systems.\n\n## Reference(s):\n\n- [ClawHub skill release](https://clawhub.ai/chrischall/skills/kiaaccess-curl)\n- [Ready-to-run requests](references/requests.md)\n\n## Skill Output:\n\n**Output Type(s):** [Shell commands, Guidance, JSON]\n\n**Output Format:** [Markdown with shell examples and JSON response excerpts]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Vehicle commands require confirmation and a subsequent status check to verify the requested change.]\n\n## Skill Version(s):\n\n1.1.4 (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.3: 4 files, 8285 bytes\n\nFiles: references/requests.md (9510b), skill-card.md (1877b), SKILL.md (4737b), _meta.json (133b)\n\nFile v1.1.3:SKILL.md\n\n---\nname: kiaaccess-curl\ndescription: Query and command a Kia vehicle directly with curl against the Kia Owners API (api.owners.kia.com), without running the MCP server. Use when the user wants a one-off read of their Kia's status, location, or EV charge state, or to lock/unlock/start climate from the shell — \"check my Kia\", \"is the car locked\", \"what's the EV9 charge\", \"lock the car from the terminal\". Requires KIA_USERNAME/KIA_PASSWORD and a one-time SMS/email MFA bootstrap.\n---\n\n# Kia Owners API via curl\n\nThe Kia Access app's API is reachable server-side — no browser, no bridge, no\nextension. This skill talks to it directly with `curl`.\n\nPrefer the `kiaaccess-mcp` server for anything conversational or repeated; use\nthis for one-off shell work, debugging, or when the server isn't running.\n\n**Ready-to-run request bodies and `jq` recipes: `references/requests.md`.**\nFull shape reference (verified live): `../../docs/KIA-API.md`.\n\n## Setup\n\n```bash\nexport KIA_USERNAME='you@example.com'\nexport KIA_PASSWORD='…'\nexport KIA_DEVICE=$(uuidgen)        # keep this stable across runs\n```\n\n## Two rules that will bite you\n\n1. **Every request needs an RFC-1123 `date` header.** Omit it and you get\n   `errorCode 9200 \"Missing mandatory data in header\"` — a message that does not\n   name the culprit. Regenerate it per request; a stale one is rejected.\n2. **HTTP is 200 even on failure.** Success is `status.statusCode == 0` in the\n   *body*. Never branch on the HTTP code.\n\nSource `references/requests.md`'s `kia_headers` helper rather than hand-rolling\nheaders — it handles both.\n\n## Auth: one-time MFA, then silent refresh\n\n`authUser` → `sendOTP` → `verifyOTP` yields a **`sid`** (session, short-lived)\nand an **`rmtoken`** (refresh, durable), both as *response headers*.\n\nAfterwards, `authUser` with the `rmtoken` header mints a fresh `sid` with **no\nMFA**. So you do the SMS dance once and then never again — save the `rmtoken`.\n\n> **Never retry a rejected login.** `errorCode 1001` (bad credentials) or `1037`\n> (bad email) increments `payload.loginAttempt`; enough failures set\n> `enforceRecaptcha` and **permanently break shell-based login**. Fix the\n> credential and try once.\n\nStore the `rmtoken` at `$KIA_SESSION` (default `~/.kiaaccess-mcp/curl-session.json`)\nwith `chmod 600`. It is a credential: it re-authenticates the account without a\npassword prompt.\n\n> Do **not** write it to `~/.kiaaccess-mcp/session.json`. That path belongs to the\n> `kiaaccess-mcp` server, whose store is keyed by `accountId` with a different\n> schema — overwriting it corrupts the server's session and forces it back\n> through MFA.\n\n## Calling\n\nReads and commands take `sid` (+ `vinkey` for vehicle-scoped calls). Get the\n`vinkey` from `ownr/gvl` → `payload.vehicleSummary[0].vehicleKey`.\n\n| Want | Endpoint |\n| --- | --- |\n| vehicles | `GET ownr/gvl` |\n| status (cached) | `POST cmm/gvi` |\n| status (force refresh) | `POST rems/rvs` |\n| EV charge targets | `GET evc/gts` |\n| lock / unlock | `GET rems/door/lock` / `rems/door/unlock` |\n| climate on / off | `POST rems/start` / `GET rems/stop` |\n| charge start / stop / limits | `POST evc/charge` / `GET evc/cancel` / `POST evc/sts` |\n\n## Confirming a command actually worked\n\n**A `statusCode: 0` means \"accepted\", not \"done\".**\n\n- **Do not poll `cmm/gts`.** Despite taking an `xid`, it returns global flags and\n  never reports per-action completion — polled through a real lock it never\n  changed.\n- **Re-read `cmm/gvi` and diff the field.** That is the only proof. Allow ~30–60s.\n\nFields to diff:\n\n| Command | Field |\n| --- | --- |\n| lock / unlock | `vehicleStatus.doorLock` |\n| climate on / off | `vehicleStatus.climate.airCtrl` and `vehicleStatus.ign3` |\n\nThree traps when writing that comparison:\n\n- **`syncDate` advances on every read.** Include it and *every* command looks\n  successful. Exclude it.\n- **There is no `airCtrlOn`.** Climate is nested under `vehicleStatus.climate`,\n  and the whole block is **absent** unless you request `cmm/gvi` with\n  `vehicleConfigReq.airTempRange: \"1\"` and `seatHeatCoolOption: \"1\"`.\n- **On an EV, `engine` stays `false`** with climate running — use `ign3`.\n\n## Verification status\n\nEvery endpoint here was verified live against a 2024 EV9 — all reads, the door\nand climate commands, and (against a plugged-in car) `evc/charge`, `evc/cancel`\nand `evc/sts`, each proven by a re-read rather than a 200.\n\nCharging proof fields live under `vehicleStatus.evStatus`: `batteryCharge`\n(true while charging), `batteryStatus` (SOC %), `batteryPlugin`. `evc/sts` is\nproven by re-reading `evc/gts`.\n\nOne caveat: **`rems/start`'s temperature may not apply** — a start requesting 70\nleft `airTemp.value` at 72. Treat it as best-effort.\n\nFile v1.1.3:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"kiaaccess-curl\",\n  \"version\": \"1.1.3\",\n  \"publishedAt\": 1790991704627\n}\n\nFile v1.1.3:references/requests.md\n\n# Ready-to-run requests\n\nEvery shape here was verified live on 2026-07-27 against a 2024 Kia EV9 —\nincluding the `evc/*` charging commands, run against the car while plugged in.\n\n## Header helper\n\nSource this first. It regenerates the mandatory `date` per call and keeps the\ndevice id stable.\n\n```bash\nKIA_BASE='https://api.owners.kia.com/apigw/v1'\n: \"${KIA_DEVICE:?export KIA_DEVICE=\\$(uuidgen) first}\"\n\n# Session file for THIS skill. Deliberately NOT ~/.kiaaccess-mcp/session.json —\n# that path belongs to the MCP server, whose store is keyed by accountId with a\n# different schema (src/session.ts). Writing this skill's flat\n# {rmtoken, deviceId} there corrupts the server's session and forces it back\n# through MFA.\nKIA_SESSION=\"${KIA_CURL_SESSION:-$HOME/.kiaaccess-mcp/curl-session.json}\"\n\n# Builds the header list into the KIA_HDRS array. Extra headers passed as args.\nkia_headers() {\n  local z sign off h\n  z=$(date +%z)                       # e.g. -0800, +0530\n  sign=${z:0:1}\n  # `10#` forces base 10. Without it, bash reads a leading-zero offset such as\n  # `08`/`09` as OCTAL and dies with \"value too great for base\" — so this breaks\n  # in US Pacific winter, Alaska, Japan, Korea and China. zsh does not have the\n  # problem, which is a good way to ship it broken without noticing.\n  off=$(( 10#${z:1:2} ))\n  [ \"$sign\" = \"-\" ] && off=$(( 0 - off ))\n\n  KIA_HDRS=()\n  for h in \\\n    \"content-type: application/json;charset=utf-8\" \\\n    \"accept: application/json\" \\\n    \"accept-language: en-US,en;q=0.9\" \\\n    \"accept-charset: utf-8\" \\\n    \"apptype: L\" \"appversion: 7.22.0\" \"clientid: SPACL716-APL\" \\\n    \"clientuuid: ${KIA_DEVICE}\" \"deviceid: ${KIA_DEVICE}\" \\\n    \"from: SPA\" \"host: api.owners.kia.com\" \"language: 0\" \\\n    \"offset: ${off}\" \"ostype: iOS\" \"osversion: 15.8.5\" \"phonebrand: iPhone\" \\\n    \"secretkey: sydnat-9kykci-Kuhtep-h5nK\" \"to: APIGW\" \"tokentype: A\" \\\n    \"date: $(LC_ALL=C date -u '+%a, %d %b %Y %H:%M:%S GMT')\" \\\n    \"user-agent: KIAPrimo_iOS/37 CFNetwork/1335.0.3.4 Darwin/21.6.0\" \\\n    \"$@\"\n  do\n    KIA_HDRS+=(-H \"$h\")\n  done\n}\n\n# curl wrapper: kia_curl <method> <path> [body] [-- extra-header ...]\nkia_curl() {\n  local method=\"$1\" path=\"$2\" body=\"${3:-}\"\n  shift 2; [ $# -gt 0 ] && shift          # drop the body arg when present\n  [ \"${1:-}\" = \"--\" ] && shift\n  kia_headers \"$@\"\n  curl -sS -X \"$method\" \"${KIA_BASE}/${path}\" \"${KIA_HDRS[@]}\" \\\n    ${body:+--data \"$body\"} -D /tmp/kia_hdrs --compressed\n}\n```\n\nBoth functions work under bash and zsh. **Test under `bash` if you change them** —\nthe octal trap above bites only bash, so zsh-only testing hides it. An array is\nused rather than piping headers through `sed`, which avoids depending on GNU\nsed's `\\n`-in-replacement behaviour.\n\n`secretkey` is a **static app constant**, not a user secret — it is the same for\nevery install.\n\n## 1. Login (one-time MFA)\n\n```bash\n# Step 1 — authenticate. Captures otpKey (body) and xid (RESPONSE HEADER).\nkia_curl POST prof/authUser \"$(jq -nc \\\n  --arg u \"$KIA_USERNAME\" --arg p \"$KIA_PASSWORD\" \\\n  '{deviceKey:\"\",deviceType:2,userCredential:{userId:$u,password:$p},tncFlag:1}')\" \\\n  | tee /tmp/kia_auth.json | jq '.status, .payload.nextAction'\n\nOTPKEY=$(jq -r '.payload.otpKey' /tmp/kia_auth.json)\nXID=$(grep -i '^xid:' /tmp/kia_hdrs | tr -d '\\r' | cut -d' ' -f2)\n```\n\n> Stop here if `status.errorCode` is `1001` or `1037`. Fix the credential — do\n> **not** loop. Repeated failures set `enforceRecaptcha` and permanently break\n> shell login.\n\n```bash\n# Step 2 — send the code. notifytype is SMS or EMAIL.\nkia_curl POST cmm/sendOTP '{}' -- \\\n  \"otpkey: $OTPKEY\" \"notifytype: SMS\" \"xid: $XID\" | jq '.status, .payload.message'\n\n# Step 3 — verify. sid + rmtoken come back as RESPONSE HEADERS.\nread -r -p 'code: ' CODE\nkia_curl POST cmm/verifyOTP \"$(jq -nc --arg o \"$CODE\" '{otp:$o}')\" -- \\\n  \"otpkey: $OTPKEY\" \"xid: $XID\" | jq '.status'\n\nSID=$(grep -i '^sid:'     /tmp/kia_hdrs | tr -d '\\r' | cut -d' ' -f2)\nRMTOKEN=$(grep -i '^rmtoken:' /tmp/kia_hdrs | tr -d '\\r' | cut -d' ' -f2)\n\nmkdir -p \"$(dirname \"$KIA_SESSION\")\" && chmod 700 \"$(dirname \"$KIA_SESSION\")\"\njq -nc --arg r \"$RMTOKEN\" --arg d \"$KIA_DEVICE\" '{rmtoken:$r,deviceId:$d}' \\\n  > \"$KIA_SESSION\"\nchmod 600 \"$KIA_SESSION\"\n```\n\n`$KIA_SESSION` — **not** `session.json`. That neighbouring file is the MCP\nserver's own store (keyed by `accountId`, different schema); clobbering it sends\nthe server back through MFA.\n\n## 2. Refresh — no MFA, use this every other time\n\n```bash\nRMTOKEN=$(jq -r .rmtoken \"$KIA_SESSION\")\nKIA_DEVICE=$(jq -r .deviceId \"$KIA_SESSION\")\n\nkia_curl POST prof/authUser \"$(jq -nc \\\n  --arg u \"$KIA_USERNAME\" --arg p \"$KIA_PASSWORD\" --arg d \"$KIA_DEVICE\" \\\n  '{deviceKey:$d,deviceType:2,userCredential:{userId:$u,password:$p}}')\" \\\n  -- \"rmtoken: $RMTOKEN\" | jq '.status.statusCode'\n\nSID=$(grep -i '^sid:' /tmp/kia_hdrs | tr -d '\\r' | cut -d' ' -f2)\n```\n\nThe `rmtoken` is **not** rotated — the stored one keeps working.\n\n## 3. Reads\n\n```bash\n# Vehicle list -> vinkey\nkia_curl GET ownr/gvl '' -- \"sid: $SID\" | tee /tmp/kia_gvl.json \\\n  | jq '.payload.vehicleSummary[] | {nickName, modelYear, modelName, mileage, vehicleKey}'\nVIN=$(jq -r '.payload.vehicleSummary[0].vehicleKey' /tmp/kia_gvl.json)\n```\n\n```bash\n# Cached status. airTempRange/seatHeatCoolOption = \"1\" or the climate block is ABSENT.\nGVI=$(jq -nc --arg v \"$VIN\" '{\n  vehicleConfigReq:{airTempRange:\"1\",maintenance:\"1\",seatHeatCoolOption:\"1\",\n                    vehicle:\"1\",vehicleFeature:\"1\"},\n  vehicleInfoReq:{drivingActivty:\"0\",dtc:\"1\",enrollment:\"1\",functionalCards:\"0\",\n                  location:\"1\",vehicleStatus:\"1\",weather:\"0\"},\n  vinKey:[$v]}')\n\nkia_curl POST cmm/gvi \"$GVI\" -- \"sid: $SID\" \"vinkey: $VIN\" > /tmp/kia_gvi.json\n\n# The fields worth looking at\njq '.payload.vehicleInfoList[0].lastVehicleInfo.vehicleStatusRpt.vehicleStatus\n    | {doorLock, ign3, engine,\n       climate: {airCtrl: .climate.airCtrl, temp: .climate.airTemp.value},\n       battery: .evStatus.batteryStatus,\n       range:   .evStatus.drvDistance[0].rangeByFuel.totalAvailableRange.value,\n       synced:  .syncDate.utc}' /tmp/kia_gvi.json\n```\n\n`drivingActivty` is **misspelled in Kia's API**. Correcting it drops the field.\n\n```bash\n# Location\njq '.payload.vehicleInfoList[0].lastVehicleInfo.location\n    | {lat: .coord.lat, lon: .coord.lon, synced: .syncDate.utc}' /tmp/kia_gvi.json\n\n# Force a fresh read from the vehicle (slow — wakes the telematics unit)\nkia_curl POST rems/rvs '{\"requestType\":0}' -- \"sid: $SID\" \"vinkey: $VIN\" | jq '.status'\n\n# EV charge targets (AC and DC)\nkia_curl GET evc/gts '' -- \"sid: $SID\" \"vinkey: $VIN\" \\\n  | jq '.payload.targetSOClist[] | {plugType, targetSOClevel}'\n```\n\n## 4. Commands\n\n```bash\n# Snapshot the fields you intend to prove changed — NEVER include syncDate.\nkia_state() {\n  kia_curl POST cmm/gvi \"$GVI\" -- \"sid: $SID\" \"vinkey: $VIN\" \\\n    | jq -c '.payload.vehicleInfoList[0].lastVehicleInfo.vehicleStatusRpt.vehicleStatus\n             | {doorLock, ign3, airCtrl: .climate.airCtrl}'\n}\nBEFORE=$(kia_state); echo \"before: $BEFORE\"\n```\n\n```bash\n# Doors — VERIFIED\nkia_curl GET rems/door/lock   '' -- \"sid: $SID\" \"vinkey: $VIN\" | jq '.status.statusCode'\nkia_curl GET rems/door/unlock '' -- \"sid: $SID\" \"vinkey: $VIN\" | jq '.status.statusCode'\n\n# Climate on — VERIFIED. duration is minutes; temperature is best-effort (see below).\nkia_curl POST rems/start \"$(jq -nc '{remoteClimate:{\n    airTemp:{unit:1,value:\"70\"}, airCtrl:true, defrost:false,\n    heatingAccessory:{rearWindow:0,sideMirror:0,steeringWheel:0,steeringWheelStep:0},\n    ignitionOnDuration:{unit:4,value:10}}}')\" \\\n  -- \"sid: $SID\" \"vinkey: $VIN\" | jq '.status.statusCode'\n\n# Climate off — VERIFIED\nkia_curl GET rems/stop '' -- \"sid: $SID\" \"vinkey: $VIN\" | jq '.status.statusCode'\n```\n\nOmit `heatVentSeat` unless you know the car supports the seats you name — Kia\nvalidates seat capability per vehicle.\n\n```bash\n# Charging — VERIFIED against a plugged-in EV9.\n# Proof: vehicleStatus.evStatus.batteryCharge flips; evc/sts proven via evc/gts.\nkia_curl POST evc/charge '{\"chargeRatio\":100}' -- \"sid: $SID\" \"vinkey: $VIN\"\nkia_curl GET  evc/cancel ''                    -- \"sid: $SID\" \"vinkey: $VIN\"\n\n# Send BOTH plug types — evc/sts replaces the list, so omitting one drops it.\nkia_curl POST evc/sts '{\"targetSOClist\":[{\"plugType\":0,\"targetSOClevel\":90},\n                                          {\"plugType\":1,\"targetSOClevel\":80}]}' \\\n  -- \"sid: $SID\" \"vinkey: $VIN\"\n\n# Charging state, for proving the above\nkia_curl POST cmm/gvi \"$GVI\" -- \"sid: $SID\" \"vinkey: $VIN\" \\\n  | jq '.payload.vehicleInfoList[0].lastVehicleInfo.vehicleStatusRpt.vehicleStatus.evStatus\n        | {batteryCharge, batteryStatus, batteryPlugin}'\n```\n\n## 5. Prove it landed\n\n```bash\n# Poll the re-read. This — not cmm/gts — is the proof.\nfor i in $(seq 1 9); do\n  sleep 10\n  AFTER=$(kia_state)\n  [ \"$AFTER\" != \"$BEFORE\" ] && { echo \"changed: $BEFORE -> $AFTER\"; break; }\n  [ \"$i\" = 9 ] && echo \"NO observed change in ~90s: $AFTER\"\ndone\n```\n\n`plugType` 0 and 1 are the two charge connectors; `targetSOClevel` is a percentage.\n\n## Error quick-reference\n\n| errorCode | Meaning | Do |\n| --- | --- | --- |\n| `0` | success | — |\n| `9200` | missing mandatory header | regenerate `date`; it must be fresh RFC-1123 |\n| `1001` | invalid email or password | fix it; **do not retry in a loop** |\n| `1037` | invalid email address | check the address format/domain |\n\nSession expired (a previously-working `sid` starts failing) → re-run §2. It needs\nno MFA.\n\nFile v1.1.3:skill-card.md\n\n## Description:\n\nHelps agents use curl to check Kia vehicle status and EV charging, and to issue authorized door, climate, and charging commands through the Kia Owners API.\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\nKia owners and authorized operators use this skill for one-off vehicle status and charging checks or remote door, climate, and charging controls from a shell.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Saved refresh tokens and temporary response files can expose account credentials and sensitive vehicle or location data on a shared machine.\n\nMitigation: Use a private machine, restrict or avoid persistent token storage, and remove /tmp/kia_* files after use.\n\nRisk: Door, climate, and charging commands can change the vehicle's physical state; an accepted response does not prove completion.\n\nMitigation: Run commands only with the owner's authorization and re-check vehicle status to confirm the intended result.\n\n## Reference(s):\n\n- [ClawHub skill release](https://clawhub.ai/chrischall/skills/kiaaccess-curl)\n- [Ready-to-run requests](references/requests.md)\n\n## Skill Output:\n\n**Output Type(s):** [Shell commands, Guidance]\n\n**Output Format:** [Markdown with shell commands and JSON response examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Vehicle commands require authorization and a follow-up status check to confirm completion.]\n\n## Skill Version(s):\n\n1.1.3 (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.1.2: 4 files, 8359 bytes\n\nFiles: references/requests.md (9510b), skill-card.md (2059b), SKILL.md (4737b), _meta.json (133b)\n\nFile v1.1.2:SKILL.md\n\n---\nname: kiaaccess-curl\ndescription: Query and command a Kia vehicle directly with curl against the Kia Owners API (api.owners.kia.com), without running the MCP server. Use when the user wants a one-off read of their Kia's status, location, or EV charge state, or to lock/unlock/start climate from the shell — \"check my Kia\", \"is the car locked\", \"what's the EV9 charge\", \"lock the car from the terminal\". Requires KIA_USERNAME/KIA_PASSWORD and a one-time SMS/email MFA bootstrap.\n---\n\n# Kia Owners API via curl\n\nThe Kia Access app's API is reachable server-side — no browser, no bridge, no\nextension. This skill talks to it directly with `curl`.\n\nPrefer the `kiaaccess-mcp` server for anything conversational or repeated; use\nthis for one-off shell work, debugging, or when the server isn't running.\n\n**Ready-to-run request bodies and `jq` recipes: `references/requests.md`.**\nFull shape reference (verified live): `../../docs/KIA-API.md`.\n\n## Setup\n\n```bash\nexport KIA_USERNAME='you@example.com'\nexport KIA_PASSWORD='…'\nexport KIA_DEVICE=$(uuidgen)        # keep this stable across runs\n```\n\n## Two rules that will bite you\n\n1. **Every request needs an RFC-1123 `date` header.** Omit it and you get\n   `errorCode 9200 \"Missing mandatory data in header\"` — a message that does not\n   name the culprit. Regenerate it per request; a stale one is rejected.\n2. **HTTP is 200 even on failure.** Success is `status.statusCode == 0` in the\n   *body*. Never branch on the HTTP code.\n\nSource `references/requests.md`'s `kia_headers` helper rather than hand-rolling\nheaders — it handles both.\n\n## Auth: one-time MFA, then silent refresh\n\n`authUser` → `sendOTP` → `verifyOTP` yields a **`sid`** (session, short-lived)\nand an **`rmtoken`** (refresh, durable), both as *response headers*.\n\nAfterwards, `authUser` with the `rmtoken` header mints a fresh `sid` with **no\nMFA**. So you do the SMS dance once and then never again — save the `rmtoken`.\n\n> **Never retry a rejected login.** `errorCode 1001` (bad credentials) or `1037`\n> (bad email) increments `payload.loginAttempt`; enough failures set\n> `enforceRecaptcha` and **permanently break shell-based login**. Fix the\n> credential and try once.\n\nStore the `rmtoken` at `$KIA_SESSION` (default `~/.kiaaccess-mcp/curl-session.json`)\nwith `chmod 600`. It is a credential: it re-authenticates the account without a\npassword prompt.\n\n> Do **not** write it to `~/.kiaaccess-mcp/session.json`. That path belongs to the\n> `kiaaccess-mcp` server, whose store is keyed by `accountId` with a different\n> schema — overwriting it corrupts the server's session and forces it back\n> through MFA.\n\n## Calling\n\nReads and commands take `sid` (+ `vinkey` for vehicle-scoped calls). Get the\n`vinkey` from `ownr/gvl` → `payload.vehicleSummary[0].vehicleKey`.\n\n| Want | Endpoint |\n| --- | --- |\n| vehicles | `GET ownr/gvl` |\n| status (cached) | `POST cmm/gvi` |\n| status (force refresh) | `POST rems/rvs` |\n| EV charge targets | `GET evc/gts` |\n| lock / unlock | `GET rems/door/lock` / `rems/door/unlock` |\n| climate on / off | `POST rems/start` / `GET rems/stop` |\n| charge start / stop / limits | `POST evc/charge` / `GET evc/cancel` / `POST evc/sts` |\n\n## Confirming a command actually worked\n\n**A `statusCode: 0` means \"accepted\", not \"done\".**\n\n- **Do not poll `cmm/gts`.** Despite taking an `xid`, it returns global flags and\n  never reports per-action completion — polled through a real lock it never\n  changed.\n- **Re-read `cmm/gvi` and diff the field.** That is the only proof. Allow ~30–60s.\n\nFields to diff:\n\n| Command | Field |\n| --- | --- |\n| lock / unlock | `vehicleStatus.doorLock` |\n| climate on / off | `vehicleStatus.climate.airCtrl` and `vehicleStatus.ign3` |\n\nThree traps when writing that comparison:\n\n- **`syncDate` advances on every read.** Include it and *every* command looks\n  successful. Exclude it.\n- **There is no `airCtrlOn`.** Climate is nested under `vehicleStatus.climate`,\n  and the whole block is **absent** unless you request `cmm/gvi` with\n  `vehicleConfigReq.airTempRange: \"1\"` and `seatHeatCoolOption: \"1\"`.\n- **On an EV, `engine` stays `false`** with climate running — use `ign3`.\n\n## Verification status\n\nEvery endpoint here was verified live against a 2024 EV9 — all reads, the door\nand climate commands, and (against a plugged-in car) `evc/charge`, `evc/cancel`\nand `evc/sts`, each proven by a re-read rather than a 200.\n\nCharging proof fields live under `vehicleStatus.evStatus`: `batteryCharge`\n(true while charging), `batteryStatus` (SOC %), `batteryPlugin`. `evc/sts` is\nproven by re-reading `evc/gts`.\n\nOne caveat: **`rems/start`'s temperature may not apply** — a start requesting 70\nleft `airTemp.value` at 72. Treat it as best-effort.\n\nFile v1.1.2:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"kiaaccess-curl\",\n  \"version\": \"1.1.2\",\n  \"publishedAt\": 1790787444839\n}\n\nFile v1.1.2:references/requests.md\n\n# Ready-to-run requests\n\nEvery shape here was verified live on 2026-07-27 against a 2024 Kia EV9 —\nincluding the `evc/*` charging commands, run against the car while plugged in.\n\n## Header helper\n\nSource this first. It regenerates the mandatory `date` per call and keeps the\ndevice id stable.\n\n```bash\nKIA_BASE='https://api.owners.kia.com/apigw/v1'\n: \"${KIA_DEVICE:?export KIA_DEVICE=\\$(uuidgen) first}\"\n\n# Session file for THIS skill. Deliberately NOT ~/.kiaaccess-mcp/session.json —\n# that path belongs to the MCP server, whose store is keyed by accountId with a\n# different schema (src/session.ts). Writing this skill's flat\n# {rmtoken, deviceId} there corrupts the server's session and forces it back\n# through MFA.\nKIA_SESSION=\"${KIA_CURL_SESSION:-$HOME/.kiaaccess-mcp/curl-session.json}\"\n\n# Builds the header list into the KIA_HDRS array. Extra headers passed as args.\nkia_headers() {\n  local z sign off h\n  z=$(date +%z)                       # e.g. -0800, +0530\n  sign=${z:0:1}\n  # `10#` forces base 10. Without it, bash reads a leading-zero offset such as\n  # `08`/`09` as OCTAL and dies with \"value too great for base\" — so this breaks\n  # in US Pacific winter, Alaska, Japan, Korea and China. zsh does not have the\n  # problem, which is a good way to ship it broken without noticing.\n  off=$(( 10#${z:1:2} ))\n  [ \"$sign\" = \"-\" ] && off=$(( 0 - off ))\n\n  KIA_HDRS=()\n  for h in \\\n    \"content-type: application/json;charset=utf-8\" \\\n    \"accept: application/json\" \\\n    \"accept-language: en-US,en;q=0.9\" \\\n    \"accept-charset: utf-8\" \\\n    \"apptype: L\" \"appversion: 7.22.0\" \"clientid: SPACL716-APL\" \\\n    \"clientuuid: ${KIA_DEVICE}\" \"deviceid: ${KIA_DEVICE}\" \\\n    \"from: SPA\" \"host: api.owners.kia.com\" \"language: 0\" \\\n    \"offset: ${off}\" \"ostype: iOS\" \"osversion: 15.8.5\" \"phonebrand: iPhone\" \\\n    \"secretkey: sydnat-9kykci-Kuhtep-h5nK\" \"to: APIGW\" \"tokentype: A\" \\\n    \"date: $(LC_ALL=C date -u '+%a, %d %b %Y %H:%M:%S GMT')\" \\\n    \"user-agent: KIAPrimo_iOS/37 CFNetwork/1335.0.3.4 Darwin/21.6.0\" \\\n    \"$@\"\n  do\n    KIA_HDRS+=(-H \"$h\")\n  done\n}\n\n# curl wrapper: kia_curl <method> <path> [body] [-- extra-header ...]\nkia_curl() {\n  local method=\"$1\" path=\"$2\" body=\"${3:-}\"\n  shift 2; [ $# -gt 0 ] && shift          # drop the body arg when present\n  [ \"${1:-}\" = \"--\" ] && shift\n  kia_headers \"$@\"\n  curl -sS -X \"$method\" \"${KIA_BASE}/${path}\" \"${KIA_HDRS[@]}\" \\\n    ${body:+--data \"$body\"} -D /tmp/kia_hdrs --compressed\n}\n```\n\nBoth functions work under bash and zsh. **Test under `bash` if you change them** —\nthe octal trap above bites only bash, so zsh-only testing hides it. An array is\nused rather than piping headers through `sed`, which avoids depending on GNU\nsed's `\\n`-in-replacement behaviour.\n\n`secretkey` is a **static app constant**, not a user secret — it is the same for\nevery install.\n\n## 1. Login (one-time MFA)\n\n```bash\n# Step 1 — authenticate. Captures otpKey (body) and xid (RESPONSE HEADER).\nkia_curl POST prof/authUser \"$(jq -nc \\\n  --arg u \"$KIA_USERNAME\" --arg p \"$KIA_PASSWORD\" \\\n  '{deviceKey:\"\",deviceType:2,userCredential:{userId:$u,password:$p},tncFlag:1}')\" \\\n  | tee /tmp/kia_auth.json | jq '.status, .payload.nextAction'\n\nOTPKEY=$(jq -r '.payload.otpKey' /tmp/kia_auth.json)\nXID=$(grep -i '^xid:' /tmp/kia_hdrs | tr -d '\\r' | cut -d' ' -f2)\n```\n\n> Stop here if `status.errorCode` is `1001` or `1037`. Fix the credential — do\n> **not** loop. Repeated failures set `enforceRecaptcha` and permanently break\n> shell login.\n\n```bash\n# Step 2 — send the code. notifytype is SMS or EMAIL.\nkia_curl POST cmm/sendOTP '{}' -- \\\n  \"otpkey: $OTPKEY\" \"notifytype: SMS\" \"xid: $XID\" | jq '.status, .payload.message'\n\n# Step 3 — verify. sid + rmtoken come back as RESPONSE HEADERS.\nread -r -p 'code: ' CODE\nkia_curl POST cmm/verifyOTP \"$(jq -nc --arg o \"$CODE\" '{otp:$o}')\" -- \\\n  \"otpkey: $OTPKEY\" \"xid: $XID\" | jq '.status'\n\nSID=$(grep -i '^sid:'     /tmp/kia_hdrs | tr -d '\\r' | cut -d' ' -f2)\nRMTOKEN=$(grep -i '^rmtoken:' /tmp/kia_hdrs | tr -d '\\r' | cut -d' ' -f2)\n\nmkdir -p \"$(dirname \"$KIA_SESSION\")\" && chmod 700 \"$(dirname \"$KIA_SESSION\")\"\njq -nc --arg r \"$RMTOKEN\" --arg d \"$KIA_DEVICE\" '{rmtoken:$r,deviceId:$d}' \\\n  > \"$KIA_SESSION\"\nchmod 600 \"$KIA_SESSION\"\n```\n\n`$KIA_SESSION` — **not** `session.json`. That neighbouring file is the MCP\nserver's own store (keyed by `accountId`, different schema); clobbering it sends\nthe server back through MFA.\n\n## 2. Refresh — no MFA, use this every other time\n\n```bash\nRMTOKEN=$(jq -r .rmtoken \"$KIA_SESSION\")\nKIA_DEVICE=$(jq -r .deviceId \"$KIA_SESSION\")\n\nkia_curl POST prof/authUser \"$(jq -nc \\\n  --arg u \"$KIA_USERNAME\" --arg p \"$KIA_PASSWORD\" --arg d \"$KIA_DEVICE\" \\\n  '{deviceKey:$d,deviceType:2,userCredential:{userId:$u,password:$p}}')\" \\\n  -- \"rmtoken: $RMTOKEN\" | jq '.status.statusCode'\n\nSID=$(grep -i '^sid:' /tmp/kia_hdrs | tr -d '\\r' | cut -d' ' -f2)\n```\n\nThe `rmtoken` is **not** rotated — the stored one keeps working.\n\n## 3. Reads\n\n```bash\n# Vehicle list -> vinkey\nkia_curl GET ownr/gvl '' -- \"sid: $SID\" | tee /tmp/kia_gvl.json \\\n  | jq '.payload.vehicleSummary[] | {nickName, modelYear, modelName, mileage, vehicleKey}'\nVIN=$(jq -r '.payload.vehicleSummary[0].vehicleKey' /tmp/kia_gvl.json)\n```\n\n```bash\n# Cached status. airTempRange/seatHeatCoolOption = \"1\" or the climate block is ABSENT.\nGVI=$(jq -nc --arg v \"$VIN\" '{\n  vehicleConfigReq:{airTempRange:\"1\",maintenance:\"1\",seatHeatCoolOption:\"1\",\n                    vehicle:\"1\",vehicleFeature:\"1\"},\n  vehicleInfoReq:{drivingActivty:\"0\",dtc:\"1\",enrollment:\"1\",functionalCards:\"0\",\n                  location:\"1\",vehicleStatus:\"1\",weather:\"0\"},\n  vinKey:[$v]}')\n\nkia_curl POST cmm/gvi \"$GVI\" -- \"sid: $SID\" \"vinkey: $VIN\" > /tmp/kia_gvi.json\n\n# The fields worth looking at\njq '.payload.vehicleInfoList[0].lastVehicleInfo.vehicleStatusRpt.vehicleStatus\n    | {doorLock, ign3, engine,\n       climate: {airCtrl: .climate.airCtrl, temp: .climate.airTemp.value},\n       battery: .evStatus.batteryStatus,\n       range:   .evStatus.drvDistance[0].rangeByFuel.totalAvailableRange.value,\n       synced:  .syncDate.utc}' /tmp/kia_gvi.json\n```\n\n`drivingActivty` is **misspelled in Kia's API**. Correcting it drops the field.\n\n```bash\n# Location\njq '.payload.vehicleInfoList[0].lastVehicleInfo.location\n    | {lat: .coord.lat, lon: .coord.lon, synced: .syncDate.utc}' /tmp/kia_gvi.json\n\n# Force a fresh read from the vehicle (slow — wakes the telematics unit)\nkia_curl POST rems/rvs '{\"requestType\":0}' -- \"sid: $SID\" \"vinkey: $VIN\" | jq '.status'\n\n# EV charge targets (AC and DC)\nkia_curl GET evc/gts '' -- \"sid: $SID\" \"vinkey: $VIN\" \\\n  | jq '.payload.targetSOClist[] | {plugType, targetSOClevel}'\n```\n\n## 4. Commands\n\n```bash\n# Snapshot the fields you intend to prove changed — NEVER include syncDate.\nkia_state() {\n  kia_curl POST cmm/gvi \"$GVI\" -- \"sid: $SID\" \"vinkey: $VIN\" \\\n    | jq -c '.payload.vehicleInfoList[0].lastVehicleInfo.vehicleStatusRpt.vehicleStatus\n             | {doorLock, ign3, airCtrl: .climate.airCtrl}'\n}\nBEFORE=$(kia_state); echo \"before: $BEFORE\"\n```\n\n```bash\n# Doors — VERIFIED\nkia_curl GET rems/door/lock   '' -- \"sid: $SID\" \"vinkey: $VIN\" | jq '.status.statusCode'\nkia_curl GET rems/door/unlock '' -- \"sid: $SID\" \"vinkey: $VIN\" | jq '.status.statusCode'\n\n# Climate on — VERIFIED. duration is minutes; temperature is best-effort (see below).\nkia_curl POST rems/start \"$(jq -nc '{remoteClimate:{\n    airTemp:{unit:1,value:\"70\"}, airCtrl:true, defrost:false,\n    heatingAccessory:{rearWindow:0,sideMirror:0,steeringWheel:0,steeringWheelStep:0},\n    ignitionOnDuration:{unit:4,value:10}}}')\" \\\n  -- \"sid: $SID\" \"vinkey: $VIN\" | jq '.status.statusCode'\n\n# Climate off — VERIFIED\nkia_curl GET rems/stop '' -- \"sid: $SID\" \"vinkey: $VIN\" | jq '.status.statusCode'\n```\n\nOmit `heatVentSeat` unless you know the car supports the seats you name — Kia\nvalidates seat capability per vehicle.\n\n```bash\n# Charging — VERIFIED against a plugged-in EV9.\n# Proof: vehicleStatus.evStatus.batteryCharge flips; evc/sts proven via evc/gts.\nkia_curl POST evc/charge '{\"chargeRatio\":100}' -- \"sid: $SID\" \"vinkey: $VIN\"\nkia_curl GET  evc/cancel ''                    -- \"sid: $SID\" \"vinkey: $VIN\"\n\n# Send BOTH plug types — evc/sts replaces the list, so omitting one drops it.\nkia_curl POST evc/sts '{\"targetSOClist\":[{\"plugType\":0,\"targetSOClevel\":90},\n                                          {\"plugType\":1,\"targetSOClevel\":80}]}' \\\n  -- \"sid: $SID\" \"vinkey: $VIN\"\n\n# Charging state, for proving the above\nkia_curl POST cmm/gvi \"$GVI\" -- \"sid: $SID\" \"vinkey: $VIN\" \\\n  | jq '.payload.vehicleInfoList[0].lastVehicleInfo.vehicleStatusRpt.vehicleStatus.evStatus\n        | {batteryCharge, batteryStatus, batteryPlugin}'\n```\n\n## 5. Prove it landed\n\n```bash\n# Poll the re-read. This — not cmm/gts — is the proof.\nfor i in $(seq 1 9); do\n  sleep 10\n  AFTER=$(kia_state)\n  [ \"$AFTER\" != \"$BEFORE\" ] && { echo \"changed: $BEFORE -> $AFTER\"; break; }\n  [ \"$i\" = 9 ] && echo \"NO observed change in ~90s: $AFTER\"\ndone\n```\n\n`plugType` 0 and 1 are the two charge connectors; `targetSOClevel` is a percentage.\n\n## Error quick-reference\n\n| errorCode | Meaning | Do |\n| --- | --- | --- |\n| `0` | success | — |\n| `9200` | missing mandatory header | regenerate `date`; it must be fresh RFC-1123 |\n| `1001` | invalid email or password | fix it; **do not retry in a loop** |\n| `1037` | invalid email address | check the address format/domain |\n\nSession expired (a previously-working `sid` starts failing) → re-run §2. It needs\nno MFA.\n\nFile v1.1.2:skill-card.md\n\n## Description:\n\nHelps agents query Kia vehicle status, location, and charging information or issue vehicle commands with curl through the Kia Owners API.\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\nKia owners and authorized operators use this skill for one-off shell-based checks of vehicle status, location, and EV charging, or to control locks, climate, and charging.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Account credentials and a durable refresh token could expose vehicle access if mishandled.\n\nMitigation: Use only on private machines, protect or rotate the saved token, and grant access only for vehicles you own or are authorized to control.\n\nRisk: Fixed temporary files can expose session headers and sensitive vehicle or location data.\n\nMitigation: Replace fixed /tmp paths with private temporary files and restrict access to stored outputs.\n\nRisk: Remote lock, climate, and charging commands affect a real vehicle; an accepted request may not have completed.\n\nMitigation: Confirm authorization before issuing commands, then reread vehicle state to verify the requested change.\n\n## Reference(s):\n\n- [Ready-to-run requests](references/requests.md)\n- [ClawHub skill release](https://clawhub.ai/chrischall/skills/kiaaccess-curl)\n\n## Skill Output:\n\n**Output Type(s):** [Shell commands, Guidance]\n\n**Output Format:** [Markdown with shell and jq examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [May return sensitive vehicle status and precise location; vehicle commands require confirmation by rereading vehicle state.]\n\n## Skill Version(s):\n\n1.1.2 (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.1.1: 4 files, 8357 bytes\n\nFiles: references/requests.md (9510b), skill-card.md (2065b), SKILL.md (4737b), _meta.json (133b)\n\nFile v1.1.1:SKILL.md\n\n---\nname: kiaaccess-curl\ndescription: Query and command a Kia vehicle directly with curl against the Kia Owners API (api.owners.kia.com), without running the MCP server. Use when the user wants a one-off read of their Kia's status, location, or EV charge state, or to lock/unlock/start climate from the shell — \"check my Kia\", \"is the car locked\", \"what's the EV9 charge\", \"lock the car from the terminal\". Requires KIA_USERNAME/KIA_PASSWORD and a one-time SMS/email MFA bootstrap.\n---\n\n# Kia Owners API via curl\n\nThe Kia Access app's API is reachable server-side — no browser, no bridge, no\nextension. This skill talks to it directly with `curl`.\n\nPrefer the `kiaaccess-mcp` server for anything conversational or repeated; use\nthis for one-off shell work, debugging, or when the server isn't running.\n\n**Ready-to-run request bodies and `jq` recipes: `references/requests.md`.**\nFull shape reference (verified live): `../../docs/KIA-API.md`.\n\n## Setup\n\n```bash\nexport KIA_USERNAME='you@example.com'\nexport KIA_PASSWORD='…'\nexport KIA_DEVICE=$(uuidgen)        # keep this stable across runs\n```\n\n## Two rules that will bite you\n\n1. **Every request needs an RFC-1123 `date` header.** Omit it and you get\n   `errorCode 9200 \"Missing mandatory data in header\"` — a message that does not\n   name the culprit. Regenerate it per request; a stale one is rejected.\n2. **HTTP is 200 even on failure.** Success is `status.statusCode == 0` in the\n   *body*. Never branch on the HTTP code.\n\nSource `references/requests.md`'s `kia_headers` helper rather than hand-rolling\nheaders — it handles both.\n\n## Auth: one-time MFA, then silent refresh\n\n`authUser` → `sendOTP` → `verifyOTP` yields a **`sid`** (session, short-lived)\nand an **`rmtoken`** (refresh, durable), both as *response headers*.\n\nAfterwards, `authUser` with the `rmtoken` header mints a fresh `sid` with **no\nMFA**. So you do the SMS dance once and then never again — save the `rmtoken`.\n\n> **Never retry a rejected login.** `errorCode 1001` (bad credentials) or `1037`\n> (bad email) increments `payload.loginAttempt`; enough failures set\n> `enforceRecaptcha` and **permanently break shell-based login**. Fix the\n> credential and try once.\n\nStore the `rmtoken` at `$KIA_SESSION` (default `~/.kiaaccess-mcp/curl-session.json`)\nwith `chmod 600`. It is a credential: it re-authenticates the account without a\npassword prompt.\n\n> Do **not** write it to `~/.kiaaccess-mcp/session.json`. That path belongs to the\n> `kiaaccess-mcp` server, whose store is keyed by `accountId` with a different\n> schema — overwriting it corrupts the server's session and forces it back\n> through MFA.\n\n## Calling\n\nReads and commands take `sid` (+ `vinkey` for vehicle-scoped calls). Get the\n`vinkey` from `ownr/gvl` → `payload.vehicleSummary[0].vehicleKey`.\n\n| Want | Endpoint |\n| --- | --- |\n| vehicles | `GET ownr/gvl` |\n| status (cached) | `POST cmm/gvi` |\n| status (force refresh) | `POST rems/rvs` |\n| EV charge targets | `GET evc/gts` |\n| lock / unlock | `GET rems/door/lock` / `rems/door/unlock` |\n| climate on / off | `POST rems/start` / `GET rems/stop` |\n| charge start / stop / limits | `POST evc/charge` / `GET evc/cancel` / `POST evc/sts` |\n\n## Confirming a command actually worked\n\n**A `statusCode: 0` means \"accepted\", not \"done\".**\n\n- **Do not poll `cmm/gts`.** Despite taking an `xid`, it returns global flags and\n  never reports per-action completion — polled through a real lock it never\n  changed.\n- **Re-read `cmm/gvi` and diff the field.** That is the only proof. Allow ~30–60s.\n\nFields to diff:\n\n| Command | Field |\n| --- | --- |\n| lock / unlock | `vehicleStatus.doorLock` |\n| climate on / off | `vehicleStatus.climate.airCtrl` and `vehicleStatus.ign3` |\n\nThree traps when writing that comparison:\n\n- **`syncDate` advances on every read.** Include it and *every* command looks\n  successful. Exclude it.\n- **There is no `airCtrlOn`.** Climate is nested under `vehicleStatus.climate`,\n  and the whole block is **absent** unless you request `cmm/gvi` with\n  `vehicleConfigReq.airTempRange: \"1\"` and `seatHeatCoolOption: \"1\"`.\n- **On an EV, `engine` stays `false`** with climate running — use `ign3`.\n\n## Verification status\n\nEvery endpoint here was verified live against a 2024 EV9 — all reads, the door\nand climate commands, and (against a plugged-in car) `evc/charge`, `evc/cancel`\nand `evc/sts`, each proven by a re-read rather than a 200.\n\nCharging proof fields live under `vehicleStatus.evStatus`: `batteryCharge`\n(true while charging), `batteryStatus` (SOC %), `batteryPlugin`. `evc/sts` is\nproven by re-reading `evc/gts`.\n\nOne caveat: **`rems/start`'s temperature may not apply** — a start requesting 70\nleft `airTemp.value` at 72. Treat it as best-effort.\n\nFile v1.1.1:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"kiaaccess-curl\",\n  \"version\": \"1.1.1\",\n  \"publishedAt\": 1790352215019\n}\n\nFile v1.1.1:references/requests.md\n\n# Ready-to-run requests\n\nEvery shape here was verified live on 2026-07-27 against a 2024 Kia EV9 —\nincluding the `evc/*` charging commands, run against the car while plugged in.\n\n## Header helper\n\nSource this first. It regenerates the mandatory `date` per call and keeps the\ndevice id stable.\n\n```bash\nKIA_BASE='https://api.owners.kia.com/apigw/v1'\n: \"${KIA_DEVICE:?export KIA_DEVICE=\\$(uuidgen) first}\"\n\n# Session file for THIS skill. Deliberately NOT ~/.kiaaccess-mcp/session.json —\n# that path belongs to the MCP server, whose store is keyed by accountId with a\n# different schema (src/session.ts). Writing this skill's flat\n# {rmtoken, deviceId} there corrupts the server's session and forces it back\n# through MFA.\nKIA_SESSION=\"${KIA_CURL_SESSION:-$HOME/.kiaaccess-mcp/curl-session.json}\"\n\n# Builds the header list into the KIA_HDRS array. Extra headers passed as args.\nkia_headers() {\n  local z sign off h\n  z=$(date +%z)                       # e.g. -0800, +0530\n  sign=${z:0:1}\n  # `10#` forces base 10. Without it, bash reads a leading-zero offset such as\n  # `08`/`09` as OCTAL and dies with \"value too great for base\" — so this breaks\n  # in US Pacific winter, Alaska, Japan, Korea and China. zsh does not have the\n  # problem, which is a good way to ship it broken without noticing.\n  off=$(( 10#${z:1:2} ))\n  [ \"$sign\" = \"-\" ] && off=$(( 0 - off ))\n\n  KIA_HDRS=()\n  for h in \\\n    \"content-type: application/json;charset=utf-8\" \\\n    \"accept: application/json\" \\\n    \"accept-language: en-US,en;q=0.9\" \\\n    \"accept-charset: utf-8\" \\\n    \"apptype: L\" \"appversion: 7.22.0\" \"clientid: SPACL716-APL\" \\\n    \"clientuuid: ${KIA_DEVICE}\" \"deviceid: ${KIA_DEVICE}\" \\\n    \"from: SPA\" \"host: api.owners.kia.com\" \"language: 0\" \\\n    \"offset: ${off}\" \"ostype: iOS\" \"osversion: 15.8.5\" \"phonebrand: iPhone\" \\\n    \"secretkey: sydnat-9kykci-Kuhtep-h5nK\" \"to: APIGW\" \"tokentype: A\" \\\n    \"date: $(LC_ALL=C date -u '+%a, %d %b %Y %H:%M:%S GMT')\" \\\n    \"user-agent: KIAPrimo_iOS/37 CFNetwork/1335.0.3.4 Darwin/21.6.0\" \\\n    \"$@\"\n  do\n    KIA_HDRS+=(-H \"$h\")\n  done\n}\n\n# curl wrapper: kia_curl <method> <path> [body] [-- extra-header ...]\nkia_curl() {\n  local method=\"$1\" path=\"$2\" body=\"${3:-}\"\n  shift 2; [ $# -gt 0 ] && shift          # drop the body arg when present\n  [ \"${1:-}\" = \"--\" ] && shift\n  kia_headers \"$@\"\n  curl -sS -X \"$method\" \"${KIA_BASE}/${path}\" \"${KIA_HDRS[@]}\" \\\n    ${body:+--data \"$body\"} -D /tmp/kia_hdrs --compressed\n}\n```\n\nBoth functions work under bash and zsh. **Test under `bash` if you change them** —\nthe octal trap above bites only bash, so zsh-only testing hides it. An array is\nused rather than piping headers through `sed`, which avoids depending on GNU\nsed's `\\n`-in-replacement behaviour.\n\n`secretkey` is a **static app constant**, not a user secret — it is the same for\nevery install.\n\n## 1. Login (one-time MFA)\n\n```bash\n# Step 1 — authenticate. Captures otpKey (body) and xid (RESPONSE HEADER).\nkia_curl POST prof/authUser \"$(jq -nc \\\n  --arg u \"$KIA_USERNAME\" --arg p \"$KIA_PASSWORD\" \\\n  '{deviceKey:\"\",deviceType:2,userCredential:{userId:$u,password:$p},tncFlag:1}')\" \\\n  | tee /tmp/kia_auth.json | jq '.status, .payload.nextAction'\n\nOTPKEY=$(jq -r '.payload.otpKey' /tmp/kia_auth.json)\nXID=$(grep -i '^xid:' /tmp/kia_hdrs | tr -d '\\r' | cut -d' ' -f2)\n```\n\n> Stop here if `status.errorCode` is `1001` or `1037`. Fix the credential — do\n> **not** loop. Repeated failures set `enforceRecaptcha` and permanently break\n> shell login.\n\n```bash\n# Step 2 — send the code. notifytype is SMS or EMAIL.\nkia_curl POST cmm/sendOTP '{}' -- \\\n  \"otpkey: $OTPKEY\" \"notifytype: SMS\" \"xid: $XID\" | jq '.status, .payload.message'\n\n# Step 3 — verify. sid + rmtoken come back as RESPONSE HEADERS.\nread -r -p 'code: ' CODE\nkia_curl POST cmm/verifyOTP \"$(jq -nc --arg o \"$CODE\" '{otp:$o}')\" -- \\\n  \"otpkey: $OTPKEY\" \"xid: $XID\" | jq '.status'\n\nSID=$(grep -i '^sid:'     /tmp/kia_hdrs | tr -d '\\r' | cut -d' ' -f2)\nRMTOKEN=$(grep -i '^rmtoken:' /tmp/kia_hdrs | tr -d '\\r' | cut -d' ' -f2)\n\nmkdir -p \"$(dirname \"$KIA_SESSION\")\" && chmod 700 \"$(dirname \"$KIA_SESSION\")\"\njq -nc --arg r \"$RMTOKEN\" --arg d \"$KIA_DEVICE\" '{rmtoken:$r,deviceId:$d}' \\\n  > \"$KIA_SESSION\"\nchmod 600 \"$KIA_SESSION\"\n```\n\n`$KIA_SESSION` — **not** `session.json`. That neighbouring file is the MCP\nserver's own store (keyed by `accountId`, different schema); clobbering it sends\nthe server back through MFA.\n\n## 2. Refresh — no MFA, use this every other time\n\n```bash\nRMTOKEN=$(jq -r .rmtoken \"$KIA_SESSION\")\nKIA_DEVICE=$(jq -r .deviceId \"$KIA_SESSION\")\n\nkia_curl POST prof/authUser \"$(jq -nc \\\n  --arg u \"$KIA_USERNAME\" --arg p \"$KIA_PASSWORD\" --arg d \"$KIA_DEVICE\" \\\n  '{deviceKey:$d,deviceType:2,userCredential:{userId:$u,password:$p}}')\" \\\n  -- \"rmtoken: $RMTOKEN\" | jq '.status.statusCode'\n\nSID=$(grep -i '^sid:' /tmp/kia_hdrs | tr -d '\\r' | cut -d' ' -f2)\n```\n\nThe `rmtoken` is **not** rotated — the stored one keeps working.\n\n## 3. Reads\n\n```bash\n# Vehicle list -> vinkey\nkia_curl GET ownr/gvl '' -- \"sid: $SID\" | tee /tmp/kia_gvl.json \\\n  | jq '.payload.vehicleSummary[] | {nickName, modelYear, modelName, mileage, vehicleKey}'\nVIN=$(jq -r '.payload.vehicleSummary[0].vehicleKey' /tmp/kia_gvl.json)\n```\n\n```bash\n# Cached status. airTempRange/seatHeatCoolOption = \"1\" or the climate block is ABSENT.\nGVI=$(jq -nc --arg v \"$VIN\" '{\n  vehicleConfigReq:{airTempRange:\"1\",maintenance:\"1\",seatHeatCoolOption:\"1\",\n                    vehicle:\"1\",vehicleFeature:\"1\"},\n  vehicleInfoReq:{drivingActivty:\"0\",dtc:\"1\",enrollment:\"1\",functionalCards:\"0\",\n                  location:\"1\",vehicleStatus:\"1\",weather:\"0\"},\n  vinKey:[$v]}')\n\nkia_curl POST cmm/gvi \"$GVI\" -- \"sid: $SID\" \"vinkey: $VIN\" > /tmp/kia_gvi.json\n\n# The fields worth looking at\njq '.payload.vehicleInfoList[0].lastVehicleInfo.vehicleStatusRpt.vehicleStatus\n    | {doorLock, ign3, engine,\n       climate: {airCtrl: .climate.airCtrl, temp: .climate.airTemp.value},\n       battery: .evStatus.batteryStatus,\n       range:   .evStatus.drvDistance[0].rangeByFuel.totalAvailableRange.value,\n       synced:  .syncDate.utc}' /tmp/kia_gvi.json\n```\n\n`drivingActivty` is **misspelled in Kia's API**. Correcting it drops the field.\n\n```bash\n# Location\njq '.payload.vehicleInfoList[0].lastVehicleInfo.location\n    | {lat: .coord.lat, lon: .coord.lon, synced: .syncDate.utc}' /tmp/kia_gvi.json\n\n# Force a fresh read from the vehicle (slow — wakes the telematics unit)\nkia_curl POST rems/rvs '{\"requestType\":0}' -- \"sid: $SID\" \"vinkey: $VIN\" | jq '.status'\n\n# EV charge targets (AC and DC)\nkia_curl GET evc/gts '' -- \"sid: $SID\" \"vinkey: $VIN\" \\\n  | jq '.payload.targetSOClist[] | {plugType, targetSOClevel}'\n```\n\n## 4. Commands\n\n```bash\n# Snapshot the fields you intend to prove changed — NEVER include syncDate.\nkia_state() {\n  kia_curl POST cmm/gvi \"$GVI\" -- \"sid: $SID\" \"vinkey: $VIN\" \\\n    | jq -c '.payload.vehicleInfoList[0].lastVehicleInfo.vehicleStatusRpt.vehicleStatus\n             | {doorLock, ign3, airCtrl: .climate.airCtrl}'\n}\nBEFORE=$(kia_state); echo \"before: $BEFORE\"\n```\n\n```bash\n# Doors — VERIFIED\nkia_curl GET rems/door/lock   '' -- \"sid: $SID\" \"vinkey: $VIN\" | jq '.status.statusCode'\nkia_curl GET rems/door/unlock '' -- \"sid: $SID\" \"vinkey: $VIN\" | jq '.status.statusCode'\n\n# Climate on — VERIFIED. duration is minutes; temperature is best-effort (see below).\nkia_curl POST rems/start \"$(jq -nc '{remoteClimate:{\n    airTemp:{unit:1,value:\"70\"}, airCtrl:true, defrost:false,\n    heatingAccessory:{rearWindow:0,sideMirror:0,steeringWheel:0,steeringWheelStep:0},\n    ignitionOnDuration:{unit:4,value:10}}}')\" \\\n  -- \"sid: $SID\" \"vinkey: $VIN\" | jq '.status.statusCode'\n\n# Climate off — VERIFIED\nkia_curl GET rems/stop '' -- \"sid: $SID\" \"vinkey: $VIN\" | jq '.status.statusCode'\n```\n\nOmit `heatVentSeat` unless you know the car supports the seats you name — Kia\nvalidates seat capability per vehicle.\n\n```bash\n# Charging — VERIFIED against a plugged-in EV9.\n# Proof: vehicleStatus.evStatus.batteryCharge flips; evc/sts proven via evc/gts.\nkia_curl POST evc/charge '{\"chargeRatio\":100}' -- \"sid: $SID\" \"vinkey: $VIN\"\nkia_curl GET  evc/cancel ''                    -- \"sid: $SID\" \"vinkey: $VIN\"\n\n# Send BOTH plug types — evc/sts replaces the list, so omitting one drops it.\nkia_curl POST evc/sts '{\"targetSOClist\":[{\"plugType\":0,\"targetSOClevel\":90},\n                                          {\"plugType\":1,\"targetSOClevel\":80}]}' \\\n  -- \"sid: $SID\" \"vinkey: $VIN\"\n\n# Charging state, for proving the above\nkia_curl POST cmm/gvi \"$GVI\" -- \"sid: $SID\" \"vinkey: $VIN\" \\\n  | jq '.payload.vehicleInfoList[0].lastVehicleInfo.vehicleStatusRpt.vehicleStatus.evStatus\n        | {batteryCharge, batteryStatus, batteryPlugin}'\n```\n\n## 5. Prove it landed\n\n```bash\n# Poll the re-read. This — not cmm/gts — is the proof.\nfor i in $(seq 1 9); do\n  sleep 10\n  AFTER=$(kia_state)\n  [ \"$AFTER\" != \"$BEFORE\" ] && { echo \"changed: $BEFORE -> $AFTER\"; break; }\n  [ \"$i\" = 9 ] && echo \"NO observed change in ~90s: $AFTER\"\ndone\n```\n\n`plugType` 0 and 1 are the two charge connectors; `targetSOClevel` is a percentage.\n\n## Error quick-reference\n\n| errorCode | Meaning | Do |\n| --- | --- | --- |\n| `0` | success | — |\n| `9200` | missing mandatory header | regenerate `date`; it must be fresh RFC-1123 |\n| `1001` | invalid email or password | fix it; **do not retry in a loop** |\n| `1037` | invalid email address | check the address format/domain |\n\nSession expired (a previously-working `sid` starts failing) → re-run §2. It needs\nno MFA.\n\nFile v1.1.1:skill-card.md\n\n## Description:\n\nHelps agents use curl to check a Kia vehicle's status, location, and charging state or request remote vehicle controls through the Kia Owners API.\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\nKia owners and developers use this skill for one-off shell checks of vehicle status, location, or EV charging, and for explicitly requested door, climate, and charging controls. It requires Kia account credentials and an initial MFA sign-in.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Saved refresh tokens and Kia credentials can grant account access.\n\nMitigation: Treat the saved token like a password, restrict file permissions, and avoid running the examples on shared machines.\n\nRisk: The skill can reveal sensitive vehicle location.\n\nMitigation: Access location only when the user requests it and avoid exposing the results to others.\n\nRisk: Remote door, climate, and charging commands can change the vehicle's physical state.\n\nMitigation: Require explicit user confirmation before each command and verify the outcome by rereading vehicle state.\n\n## Reference(s):\n\n- [Kia Owners API via curl request examples](references/requests.md)\n- [ClawHub skill release](https://clawhub.ai/chrischall/skills/kiaaccess-curl)\n\n## Skill Output:\n\n**Output Type(s):** [Shell commands, Guidance, Text]\n\n**Output Format:** [Markdown with shell commands and JSON response examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Requires Kia credentials and one-time SMS or email MFA; remote commands should be confirmed by rereading vehicle state.]\n\n## Skill Version(s):\n\n1.1.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.1.0: 4 files, 8541 bytes\n\nFiles: references/requests.md (9510b), skill-card.md (2359b), SKILL.md (4737b), _meta.json (133b)\n\nFile v1.1.0:SKILL.md\n\n---\nname: kiaaccess-curl\ndescription: Query and command a Kia vehicle directly with curl against the Kia Owners API (api.owners.kia.com), without running the MCP server. Use when the user wants a one-off read of their Kia's status, location, or EV charge state, or to lock/unlock/start climate from the shell — \"check my Kia\", \"is the car locked\", \"what's the EV9 charge\", \"lock the car from the terminal\". Requires KIA_USERNAME/KIA_PASSWORD and a one-time SMS/email MFA bootstrap.\n---\n\n# Kia Owners API via curl\n\nThe Kia Access app's API is reachable server-side — no browser, no bridge, no\nextension. This skill talks to it directly with `curl`.\n\nPrefer the `kiaaccess-mcp` server for anything conversational or repeated; use\nthis for one-off shell work, debugging, or when the server isn't running.\n\n**Ready-to-run request bodies and `jq` recipes: `references/requests.md`.**\nFull shape reference (verified live): `../../docs/KIA-API.md`.\n\n## Setup\n\n```bash\nexport KIA_USERNAME='you@example.com'\nexport KIA_PASSWORD='…'\nexport KIA_DEVICE=$(uuidgen)        # keep this stable across runs\n```\n\n## Two rules that will bite you\n\n1. **Every request needs an RFC-1123 `date` header.** Omit it and you get\n   `errorCode 9200 \"Missing mandatory data in header\"` — a message that does not\n   name the culprit. Regenerate it per request; a stale one is rejected.\n2. **HTTP is 200 even on failure.** Success is `status.statusCode == 0` in the\n   *body*. Never branch on the HTTP code.\n\nSource `references/requests.md`'s `kia_headers` helper rather than hand-rolling\nheaders — it handles both.\n\n## Auth: one-time MFA, then silent refresh\n\n`authUser` → `sendOTP` → `verifyOTP` yields a **`sid`** (session, short-lived)\nand an **`rmtoken`** (refresh, durable), both as *response headers*.\n\nAfterwards, `authUser` with the `rmtoken` header mints a fresh `sid` with **no\nMFA**. So you do the SMS dance once and then never again — save the `rmtoken`.\n\n> **Never retry a rejected login.** `errorCode 1001` (bad credentials) or `1037`\n> (bad email) increments `payload.loginAttempt`; enough failures set\n> `enforceRecaptcha` and **permanently break shell-based login**. Fix the\n> credential and try once.\n\nStore the `rmtoken` at `$KIA_SESSION` (default `~/.kiaaccess-mcp/curl-session.json`)\nwith `chmod 600`. It is a credential: it re-authenticates the account without a\npassword prompt.\n\n> Do **not** write it to `~/.kiaaccess-mcp/session.json`. That path belongs to the\n> `kiaaccess-mcp` server, whose store is keyed by `accountId` with a different\n> schema — overwriting it corrupts the server's session and forces it back\n> through MFA.\n\n## Calling\n\nReads and commands take `sid` (+ `vinkey` for vehicle-scoped calls). Get the\n`vinkey` from `ownr/gvl` → `payload.vehicleSummary[0].vehicleKey`.\n\n| Want | Endpoint |\n| --- | --- |\n| vehicles | `GET ownr/gvl` |\n| status (cached) | `POST cmm/gvi` |\n| status (force refresh) | `POST rems/rvs` |\n| EV charge targets | `GET evc/gts` |\n| lock / unlock | `GET rems/door/lock` / `rems/door/unlock` |\n| climate on / off | `POST rems/start` / `GET rems/stop` |\n| charge start / stop / limits | `POST evc/charge` / `GET evc/cancel` / `POST evc/sts` |\n\n## Confirming a command actually worked\n\n**A `statusCode: 0` means \"accepted\", not \"done\".**\n\n- **Do not poll `cmm/gts`.** Despite taking an `xid`, it returns global flags and\n  never reports per-action completion — polled through a real lock it never\n  changed.\n- **Re-read `cmm/gvi` and diff the field.** That is the only proof. Allow ~30–60s.\n\nFields to diff:\n\n| Command | Field |\n| --- | --- |\n| lock / unlock | `vehicleStatus.doorLock` |\n| climate on / off | `vehicleStatus.climate.airCtrl` and `vehicleStatus.ign3` |\n\nThree traps when writing that comparison:\n\n- **`syncDate` advances on every read.** Include it and *every* command looks\n  successful. Exclude it.\n- **There is no `airCtrlOn`.** Climate is nested under `vehicleStatus.climate`,\n  and the whole block is **absent** unless you request `cmm/gvi` with\n  `vehicleConfigReq.airTempRange: \"1\"` and `seatHeatCoolOption: \"1\"`.\n- **On an EV, `engine` stays `false`** with climate running — use `ign3`.\n\n## Verification status\n\nEvery endpoint here was verified live against a 2024 EV9 — all reads, the door\nand climate commands, and (against a plugged-in car) `evc/charge`, `evc/cancel`\nand `evc/sts`, each proven by a re-read rather than a 200.\n\nCharging proof fields live under `vehicleStatus.evStatus`: `batteryCharge`\n(true while charging), `batteryStatus` (SOC %), `batteryPlugin`. `evc/sts` is\nproven by re-reading `evc/gts`.\n\nOne caveat: **`rems/start`'s temperature may not apply** — a start requesting 70\nleft `airTemp.value` at 72. Treat it as best-effort.\n\nFile v1.1.0:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"kiaaccess-curl\",\n  \"version\": \"1.1.0\",\n  \"publishedAt\": 1790262698014\n}\n\nFile v1.1.0:references/requests.md\n\n# Ready-to-run requests\n\nEvery shape here was verified live on 2026-07-27 against a 2024 Kia EV9 —\nincluding the `evc/*` charging commands, run against the car while plugged in.\n\n## Header helper\n\nSource this first. It regenerates the mandatory `date` per call and keeps the\ndevice id stable.\n\n```bash\nKIA_BASE='https://api.owners.kia.com/apigw/v1'\n: \"${KIA_DEVICE:?export KIA_DEVICE=\\$(uuidgen) first}\"\n\n# Session file for THIS skill. Deliberately NOT ~/.kiaaccess-mcp/session.json —\n# that path belongs to the MCP server, whose store is keyed by accountId with a\n# different schema (src/session.ts). Writing this skill's flat\n# {rmtoken, deviceId} there corrupts the server's session and forces it back\n# through MFA.\nKIA_SESSION=\"${KIA_CURL_SESSION:-$HOME/.kiaaccess-mcp/curl-session.json}\"\n\n# Builds the header list into the KIA_HDRS array. Extra headers passed as args.\nkia_headers() {\n  local z sign off h\n  z=$(date +%z)                       # e.g. -0800, +0530\n  sign=${z:0:1}\n  # `10#` forces base 10. Without it, bash reads a leading-zero offset such as\n  # `08`/`09` as OCTAL and dies with \"value too great for base\" — so this breaks\n  # in US Pacific winter, Alaska, Japan, Korea and China. zsh does not have the\n  # problem, which is a good way to ship it broken without noticing.\n  off=$(( 10#${z:1:2} ))\n  [ \"$sign\" = \"-\" ] && off=$(( 0 - off ))\n\n  KIA_HDRS=()\n  for h in \\\n    \"content-type: application/json;charset=utf-8\" \\\n    \"accept: application/json\" \\\n    \"accept-language: en-US,en;q=0.9\" \\\n    \"accept-charset: utf-8\" \\\n    \"apptype: L\" \"appversion: 7.22.0\" \"clientid: SPACL716-APL\" \\\n    \"clientuuid: ${KIA_DEVICE}\" \"deviceid: ${KIA_DEVICE}\" \\\n    \"from: SPA\" \"host: api.owners.kia.com\" \"language: 0\" \\\n    \"offset: ${off}\" \"ostype: iOS\" \"osversion: 15.8.5\" \"phonebrand: iPhone\" \\\n    \"secretkey: sydnat-9kykci-Kuhtep-h5nK\" \"to: APIGW\" \"tokentype: A\" \\\n    \"date: $(LC_ALL=C date -u '+%a, %d %b %Y %H:%M:%S GMT')\" \\\n    \"user-agent: KIAPrimo_iOS/37 CFNetwork/1335.0.3.4 Darwin/21.6.0\" \\\n    \"$@\"\n  do\n    KIA_HDRS+=(-H \"$h\")\n  done\n}\n\n# curl wrapper: kia_curl <method> <path> [body] [-- extra-header ...]\nkia_curl() {\n  local method=\"$1\" path=\"$2\" body=\"${3:-}\"\n  shift 2; [ $# -gt 0 ] && shift          # drop the body arg when present\n  [ \"${1:-}\" = \"--\" ] && shift\n  kia_headers \"$@\"\n  curl -sS -X \"$method\" \"${KIA_BASE}/${path}\" \"${KIA_HDRS[@]}\" \\\n    ${body:+--data \"$body\"} -D /tmp/kia_hdrs --compressed\n}\n```\n\nBoth functions work under bash and zsh. **Test under `bash` if you change them** —\nthe octal trap above bites only bash, so zsh-only testing hides it. An array is\nused rather than piping headers through `sed`, which avoids depending on GNU\nsed's `\\n`-in-replacement behaviour.\n\n`secretkey` is a **static app constant**, not a user secret — it is the same for\nevery install.\n\n## 1. Login (one-time MFA)\n\n```bash\n# Step 1 — authenticate. Captures otpKey (body) and xid (RESPONSE HEADER).\nkia_curl POST prof/authUser \"$(jq -nc \\\n  --arg u \"$KIA_USERNAME\" --arg p \"$KIA_PASSWORD\" \\\n  '{deviceKey:\"\",deviceType:2,userCredential:{userId:$u,password:$p},tncFlag:1}')\" \\\n  | tee /tmp/kia_auth.json | jq '.status, .payload.nextAction'\n\nOTPKEY=$(jq -r '.payload.otpKey' /tmp/kia_auth.json)\nXID=$(grep -i '^xid:' /tmp/kia_hdrs | tr -d '\\r' | cut -d' ' -f2)\n```\n\n> Stop here if `status.errorCode` is `1001` or `1037`. Fix the credential — do\n> **not** loop. Repeated failures set `enforceRecaptcha` and permanently break\n> shell login.\n\n```bash\n# Step 2 — send the code. notifytype is SMS or EMAIL.\nkia_curl POST cmm/sendOTP '{}' -- \\\n  \"otpkey: $OTPKEY\" \"notifytype: SMS\" \"xid: $XID\" | jq '.status, .payload.message'\n\n# Step 3 — verify. sid + rmtoken come back as RESPONSE HEADERS.\nread -r -p 'code: ' CODE\nkia_curl POST cmm/verifyOTP \"$(jq -nc --arg o \"$CODE\" '{otp:$o}')\" -- \\\n  \"otpkey: $OTPKEY\" \"xid: $XID\" | jq '.status'\n\nSID=$(grep -i '^sid:'     /tmp/kia_hdrs | tr -d '\\r' | cut -d' ' -f2)\nRMTOKEN=$(grep -i '^rmtoken:' /tmp/kia_hdrs | tr -d '\\r' | cut -d' ' -f2)\n\nmkdir -p \"$(dirname \"$KIA_SESSION\")\" && chmod 700 \"$(dirname \"$KIA_SESSION\")\"\njq -nc --arg r \"$RMTOKEN\" --arg d \"$KIA_DEVICE\" '{rmtoken:$r,deviceId:$d}' \\\n  > \"$KIA_SESSION\"\nchmod 600 \"$KIA_SESSION\"\n```\n\n`$KIA_SESSION` — **not** `session.json`. That neighbouring file is the MCP\nserver's own store (keyed by `accountId`, different schema); clobbering it sends\nthe server back through MFA.\n\n## 2. Refresh — no MFA, use this every other time\n\n```bash\nRMTOKEN=$(jq -r .rmtoken \"$KIA_SESSION\")\nKIA_DEVICE=$(jq -r .deviceId \"$KIA_SESSION\")\n\nkia_curl POST prof/authUser \"$(jq -nc \\\n  --arg u \"$KIA_USERNAME\" --arg p \"$KIA_PASSWORD\" --arg d \"$KIA_DEVICE\" \\\n  '{deviceKey:$d,deviceType:2,userCredential:{userId:$u,password:$p}}')\" \\\n  -- \"rmtoken: $RMTOKEN\" | jq '.status.statusCode'\n\nSID=$(grep -i '^sid:' /tmp/kia_hdrs | tr -d '\\r' | cut -d' ' -f2)\n```\n\nThe `rmtoken` is **not** rotated — the stored one keeps working.\n\n## 3. Reads\n\n```bash\n# Vehicle list -> vinkey\nkia_curl GET ownr/gvl '' -- \"sid: $SID\" | tee /tmp/kia_gvl.json \\\n  | jq '.payload.vehicleSummary[] | {nickName, modelYear, modelName, mileage, vehicleKey}'\nVIN=$(jq -r '.payload.vehicleSummary[0].vehicleKey' /tmp/kia_gvl.json)\n```\n\n```bash\n# Cached status. airTempRange/seatHeatCoolOption = \"1\" or the climate block is ABSENT.\nGVI=$(jq -nc --arg v \"$VIN\" '{\n  vehicleConfigReq:{airTempRange:\"1\",maintenance:\"1\",seatHeatCoolOption:\"1\",\n                    vehicle:\"1\",vehicleFeature:\"1\"},\n  vehicleInfoReq:{drivingActivty:\"0\",dtc:\"1\",enrollment:\"1\",functionalCards:\"0\",\n                  location:\"1\",vehicleStatus:\"1\",weather:\"0\"},\n  vinKey:[$v]}')\n\nkia_curl POST cmm/gvi \"$GVI\" -- \"sid: $SID\" \"vinkey: $VIN\" > /tmp/kia_gvi.json\n\n# The fields worth looking at\njq '.payload.vehicleInfoList[0].lastVehicleInfo.vehicleStatusRpt.vehicleStatus\n    | {doorLock, ign3, engine,\n       climate: {airCtrl: .climate.airCtrl, temp: .climate.airTemp.value},\n       battery: .evStatus.batteryStatus,\n       range:   .evStatus.drvDistance[0].rangeByFuel.totalAvailableRange.value,\n       synced:  .syncDate.utc}' /tmp/kia_gvi.json\n```\n\n`drivingActivty` is **misspelled in Kia's API**. Correcting it drops the field.\n\n```bash\n# Location\njq '.payload.vehicleInfoList[0].lastVehicleInfo.location\n    | {lat: .coord.lat, lon: .coord.lon, synced: .syncDate.utc}' /tmp/kia_gvi.json\n\n# Force a fresh read from the vehicle (slow — wakes the telematics unit)\nkia_curl POST rems/rvs '{\"requestType\":0}' -- \"sid: $SID\" \"vinkey: $VIN\" | jq '.status'\n\n# EV charge targets (AC and DC)\nkia_curl GET evc/gts '' -- \"sid: $SID\" \"vinkey: $VIN\" \\\n  | jq '.payload.targetSOClist[] | {plugType, targetSOClevel}'\n```\n\n## 4. Commands\n\n```bash\n# Snapshot the fields you intend to prove changed — NEVER include syncDate.\nkia_state() {\n  kia_curl POST cmm/gvi \"$GVI\" -- \"sid: $SID\" \"vinkey: $VIN\" \\\n    | jq -c '.payload.vehicleInfoList[0].lastVehicleInfo.vehicleStatusRpt.vehicleStatus\n             | {doorLock, ign3, airCtrl: .climate.airCtrl}'\n}\nBEFORE=$(kia_state); echo \"before: $BEFORE\"\n```\n\n```bash\n# Doors — VERIFIED\nkia_curl GET rems/door/lock   '' -- \"sid: $SID\" \"vinkey: $VIN\" | jq '.status.statusCode'\nkia_curl GET rems/door/unlock '' -- \"sid: $SID\" \"vinkey: $VIN\" | jq '.status.statusCode'\n\n# Climate on — VERIFIED. duration is minutes; temperature is best-effort (see below).\nkia_curl POST rems/start \"$(jq -nc '{remoteClimate:{\n    airTemp:{unit:1,value:\"70\"}, airCtrl:true, defrost:false,\n    heatingAccessory:{rearWindow:0,sideMirror:0,steeringWheel:0,steeringWheelStep:0},\n    ignitionOnDuration:{unit:4,value:10}}}')\" \\\n  -- \"sid: $SID\" \"vinkey: $VIN\" | jq '.status.statusCode'\n\n# Climate off — VERIFIED\nkia_curl GET rems/stop '' -- \"sid: $SID\" \"vinkey: $VIN\" | jq '.status.statusCode'\n```\n\nOmit `heatVentSeat` unless you know the car supports the seats you name — Kia\nvalidates seat capability per vehicle.\n\n```bash\n# Charging — VERIFIED against a plugged-in EV9.\n# Proof: vehicleStatus.evStatus.batteryCharge flips; evc/sts proven via evc/gts.\nkia_curl POST evc/charge '{\"chargeRatio\":100}' -- \"sid: $SID\" \"vinkey: $VIN\"\nkia_curl GET  evc/cancel ''                    -- \"sid: $SID\" \"vinkey: $VIN\"\n\n# Send BOTH plug types — evc/sts replaces the list, so omitting one drops it.\nkia_curl POST evc/sts '{\"targetSOClist\":[{\"plugType\":0,\"targetSOClevel\":90},\n                                          {\"plugType\":1,\"targetSOClevel\":80}]}' \\\n  -- \"sid: $SID\" \"vinkey: $VIN\"\n\n# Charging state, for proving the above\nkia_curl POST cmm/gvi \"$GVI\" -- \"sid: $SID\" \"vinkey: $VIN\" \\\n  | jq '.payload.vehicleInfoList[0].lastVehicleInfo.vehicleStatusRpt.vehicleStatus.evStatus\n        | {batteryCharge, batteryStatus, batteryPlugin}'\n```\n\n## 5. Prove it landed\n\n```bash\n# Poll the re-read. This — not cmm/gts — is the proof.\nfor i in $(seq 1 9); do\n  sleep 10\n  AFTER=$(kia_state)\n  [ \"$AFTER\" != \"$BEFORE\" ] && { echo \"changed: $BEFORE -> $AFTER\"; break; }\n  [ \"$i\" = 9 ] && echo \"NO observed change in ~90s: $AFTER\"\ndone\n```\n\n`plugType` 0 and 1 are the two charge connectors; `targetSOClevel` is a percentage.\n\n## Error quick-reference\n\n| errorCode | Meaning | Do |\n| --- | --- | --- |\n| `0` | success | — |\n| `9200` | missing mandatory header | regenerate `date`; it must be fresh RFC-1123 |\n| `1001` | invalid email or password | fix it; **do not retry in a loop** |\n| `1037` | invalid email address | check the address format/domain |\n\nSession expired (a previously-working `sid` starts failing) → re-run §2. It needs\nno MFA.\n\nFile v1.1.0:skill-card.md\n\n## Description:\n\nQuery and command a Kia vehicle directly with curl against the Kia Owners API (api.owners.kia.com), without running the MCP server.\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, engineers, and technically comfortable Kia owners use this skill for one-off shell-based vehicle status checks, location reads, EV charging checks, and remote commands such as lock, unlock, climate start, and charging control.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: Shell examples handle Kia account credentials, refresh tokens, vehicle location, and remote vehicle controls, and the security evidence notes use of predictable temporary files.\n\nMitigation: Install only for an authorized vehicle owner, run commands in a private shell environment, replace fixed /tmp paths with an owner-only mktemp directory, and treat the saved rmtoken like a password.\n\nRisk: Remote commands affect a real vehicle, while accepted API responses do not by themselves prove the command completed.\n\nMitigation: Review each command before execution and verify results with a follow-up vehicle status read rather than relying only on HTTP 200 or statusCode 0.\n\nRisk: Repeated rejected login attempts can trigger account protections that break shell-based login.\n\nMitigation: Do not loop failed authentication attempts; correct the credential or account input before trying again.\n\n## Reference(s):\n\n- [Ready-to-run requests](artifact/references/requests.md)\n- [Kia Owners API endpoint](https://api.owners.kia.com/apigw/v1)\n\n## Skill Output:\n\n**Output Type(s):** [text, markdown, shell commands, configuration, guidance]\n\n**Output Format:** [Markdown with inline bash and jq code blocks]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Produces command snippets and operational guidance for direct curl calls; it does not run the API calls by itself.]\n\n## Skill Version(s):\n\n1.1.0 (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.0.3: 4 files, 8407 bytes\n\nFiles: references/requests.md (9510b), skill-card.md (2089b), SKILL.md (4737b), _meta.json (133b)\n\nFile v1.0.3:SKILL.md\n\n---\nname: kiaaccess-curl\ndescription: Query and command a Kia vehicle directly with curl against the Kia Owners API (api.owners.kia.com), without running the MCP server. Use when the user wants a one-off read of their Kia's status, location, or EV charge state, or to lock/unlock/start climate from the shell — \"check my Kia\", \"is the car locked\", \"what's the EV9 charge\", \"lock the car from the terminal\". Requires KIA_USERNAME/KIA_PASSWORD and a one-time SMS/email MFA bootstrap.\n---\n\n# Kia Owners API via curl\n\nThe Kia Access app's API is reachable server-side — no browser, no bridge, no\nextension. This skill talks to it directly with `curl`.\n\nPrefer the `kiaaccess-mcp` server for anything conversational or repeated; use\nthis for one-off shell work, debugging, or when the server isn't running.\n\n**Ready-to-run request bodies and `jq` recipes: `references/requests.md`.**\nFull shape reference (verified live): `../../docs/KIA-API.md`.\n\n## Setup\n\n```bash\nexport KIA_USERNAME='you@example.com'\nexport KIA_PASSWORD='…'\nexport KIA_DEVICE=$(uuidgen)        # keep this stable across runs\n```\n\n## Two rules that will bite you\n\n1. **Every request needs an RFC-1123 `date` header.** Omit it and you get\n   `errorCode 9200 \"Missing mandatory data in header\"` — a message that does not\n   name the culprit. Regenerate it per request; a stale one is rejected.\n2. **HTTP is 200 even on failure.** Success is `status.statusCode == 0` in the\n   *body*. Never branch on the HTTP code.\n\nSource `references/requests.md`'s `kia_headers` helper rather than hand-rolling\nheaders — it handles both.\n\n## Auth: one-time MFA, then silent refresh\n\n`authUser` → `sendOTP` → `verifyOTP` yields a **`sid`** (session, short-lived)\nand an **`rmtoken`** (refresh, durable), both as *response headers*.\n\nAfterwards, `authUser` with the `rmtoken` header mints a fresh `sid` with **no\nMFA**. So you do the SMS dance once and then never again — save the `rmtoken`.\n\n> **Never retry a rejected login.** `errorCode 1001` (bad credentials) or `1037`\n> (bad email) increments `payload.loginAttempt`; enough failures set\n> `enforceRecaptcha` and **permanently break shell-based login**. Fix the\n> credential and try once.\n\nStore the `rmtoken` at `$KIA_SESSION` (default `~/.kiaaccess-mcp/curl-session.json`)\nwith `chmod 600`. It is a credential: it re-authenticates the account without a\npassword prompt.\n\n> Do **not** write it to `~/.kiaaccess-mcp/session.json`. That path belongs to the\n> `kiaaccess-mcp` server, whose store is keyed by `accountId` with a different\n> schema — overwriting it corrupts the server's session and forces it back\n> through MFA.\n\n## Calling\n\nReads and commands take `sid` (+ `vinkey` for vehicle-scoped calls). Get the\n`vinkey` from `ownr/gvl` → `payload.vehicleSummary[0].vehicleKey`.\n\n| Want | Endpoint |\n| --- | --- |\n| vehicles | `GET ownr/gvl` |\n| status (cached) | `POST cmm/gvi` |\n| status (force refresh) | `POST rems/rvs` |\n| EV charge targets | `GET evc/gts` |\n| lock / unlock | `GET rems/door/lock` / `rems/door/unlock` |\n| climate on / off | `POST rems/start` / `GET rems/stop` |\n| charge start / stop / limits | `POST evc/charge` / `GET evc/cancel` / `POST evc/sts` |\n\n## Confirming a command actually worked\n\n**A `statusCode: 0` means \"accepted\", not \"done\".**\n\n- **Do not poll `cmm/gts`.** Despite taking an `xid`, it returns global flags and\n  never reports per-action completion — polled through a real lock it never\n  changed.\n- **Re-read `cmm/gvi` and diff the field.** That is the only proof. Allow ~30–60s.\n\nFields to diff:\n\n| Command | Field |\n| --- | --- |\n| lock / unlock | `vehicleStatus.doorLock` |\n| climate on / off | `vehicleStatus.climate.airCtrl` and `vehicleStatus.ign3` |\n\nThree traps when writing that comparison:\n\n- **`syncDate` advances on every read.** Include it and *every* command looks\n  successful. Exclude it.\n- **There is no `airCtrlOn`.** Climate is nested under `vehicleStatus.climate`,\n  and the whole block is **absent** unless you request `cmm/gvi` with\n  `vehicleConfigReq.airTempRange: \"1\"` and `seatHeatCoolOption: \"1\"`.\n- **On an EV, `engine` stays `false`** with climate running — use `ign3`.\n\n## Verification status\n\nEvery endpoint here was verified live against a 2024 EV9 — all reads, the door\nand climate commands, and (against a plugged-in car) `evc/charge`, `evc/cancel`\nand `evc/sts`, each proven by a re-read rather than a 200.\n\nCharging proof fields live under `vehicleStatus.evStatus`: `batteryCharge`\n(true while charging), `batteryStatus` (SOC %), `batteryPlugin`. `evc/sts` is\nproven by re-reading `evc/gts`.\n\nOne caveat: **`rems/start`'s temperature may not apply** — a start requesting 70\nleft `airTemp.value` at 72. Treat it as best-effort.\n\nFile v1.0.3:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"kiaaccess-curl\",\n  \"version\": \"1.0.3\",\n  \"publishedAt\": 1790199541783\n}\n\nFile v1.0.3:references/requests.md\n\n# Ready-to-run requests\n\nEvery shape here was verified live on 2026-07-27 against a 2024 Kia EV9 —\nincluding the `evc/*` charging commands, run against the car while plugged in.\n\n## Header helper\n\nSource this first. It regenerates the mandatory `date` per call and keeps the\ndevice id stable.\n\n```bash\nKIA_BASE='https://api.owners.kia.com/apigw/v1'\n: \"${KIA_DEVICE:?export KIA_DEVICE=\\$(uuidgen) first}\"\n\n# Session file for THIS skill. Deliberately NOT ~/.kiaaccess-mcp/session.json —\n# that path belongs to the MCP server, whose store is keyed by accountId with a\n# different schema (src/session.ts). Writing this skill's flat\n# {rmtoken, deviceId} there corrupts the server's session and forces it back\n# through MFA.\nKIA_SESSION=\"${KIA_CURL_SESSION:-$HOME/.kiaaccess-mcp/curl-session.json}\"\n\n# Builds the header list into the KIA_HDRS array. Extra headers passed as args.\nkia_headers() {\n  local z sign off h\n  z=$(date +%z)                       # e.g. -0800, +0530\n  sign=${z:0:1}\n  # `10#` forces base 10. Without it, bash reads a leading-zero offset such as\n  # `08`/`09` as OCTAL and dies with \"value too great for base\" — so this breaks\n  # in US Pacific winter, Alaska, Japan, Korea and China. zsh does not have the\n  # problem, which is a good way to ship it broken without noticing.\n  off=$(( 10#${z:1:2} ))\n  [ \"$sign\" = \"-\" ] && off=$(( 0 - off ))\n\n  KIA_HDRS=()\n  for h in \\\n    \"content-type: application/json;charset=utf-8\" \\\n    \"accept: application/json\" \\\n    \"accept-language: en-US,en;q=0.9\" \\\n    \"accept-charset: utf-8\" \\\n    \"apptype: L\" \"appversion: 7.22.0\" \"clientid: SPACL716-APL\" \\\n    \"clientuuid: ${KIA_DEVICE}\" \"deviceid: ${KIA_DEVICE}\" \\\n    \"from: SPA\" \"host: api.owners.kia.com\" \"language: 0\" \\\n    \"offset: ${off}\" \"ostype: iOS\" \"osversion: 15.8.5\" \"phonebrand: iPhone\" \\\n    \"secretkey: sydnat-9kykci-Kuhtep-h5nK\" \"to: APIGW\" \"tokentype: A\" \\\n    \"date: $(LC_ALL=C date -u '+%a, %d %b %Y %H:%M:%S GMT')\" \\\n    \"user-agent: KIAPrimo_iOS/37 CFNetwork/1335.0.3.4 Darwin/21.6.0\" \\\n    \"$@\"\n  do\n    KIA_HDRS+=(-H \"$h\")\n  done\n}\n\n# curl wrapper: kia_curl <method> <path> [body] [-- extra-header ...]\nkia_curl() {\n  local method=\"$1\" path=\"$2\" body=\"${3:-}\"\n  shift 2; [ $# -gt 0 ] && shift          # drop the body arg when present\n  [ \"${1:-}\" = \"--\" ] && shift\n  kia_headers \"$@\"\n  curl -sS -X \"$method\" \"${KIA_BASE}/${path}\" \"${KIA_HDRS[@]}\" \\\n    ${body:+--data \"$body\"} -D /tmp/kia_hdrs --compressed\n}\n```\n\nBoth functions work under bash and zsh. **Test under `bash` if you change them** —\nthe octal trap above bites only bash, so zsh-only testing hides it. An array is\nused rather than piping headers through `sed`, which avoids depending on GNU\nsed's `\\n`-in-replacement behaviour.\n\n`secretkey` is a **static app constant**, not a user secret — it is the same for\nevery install.\n\n## 1. Login (one-time MFA)\n\n```bash\n# Step 1 — authenticate. Captures otpKey (body) and xid (RESPONSE HEADER).\nkia_curl POST prof/authUser \"$(jq -nc \\\n  --arg u \"$KIA_USERNAME\" --arg p \"$KIA_PASSWORD\" \\\n  '{deviceKey:\"\",deviceType:2,userCredential:{userId:$u,password:$p},tncFlag:1}')\" \\\n  | tee /tmp/kia_auth.json | jq '.status, .payload.nextAction'\n\nOTPKEY=$(jq -r '.payload.otpKey' /tmp/kia_auth.json)\nXID=$(grep -i '^xid:' /tmp/kia_hdrs | tr -d '\\r' | cut -d' ' -f2)\n```\n\n> Stop here if `status.errorCode` is `1001` or `1037`. Fix the credential — do\n> **not** loop. Repeated failures set `enforceRecaptcha` and permanently break\n> shell login.\n\n```bash\n# Step 2 — send the code. notifytype is SMS or EMAIL.\nkia_curl POST cmm/sendOTP '{}' -- \\\n  \"otpkey: $OTPKEY\" \"notifytype: SMS\" \"xid: $XID\" | jq '.status, .payload.message'\n\n# Step 3 — verify. sid + rmtoken come back as RESPONSE HEADERS.\nread -r -p 'code: ' CODE\nkia_curl POST cmm/verifyOTP \"$(jq -nc --arg o \"$CODE\" '{otp:$o}')\" -- \\\n  \"otpkey: $OTPKEY\" \"xid: $XID\" | jq '.status'\n\nSID=$(grep -i '^sid:'     /tmp/kia_hdrs | tr -d '\\r' | cut -d' ' -f2)\nRMTOKEN=$(grep -i '^rmtoken:' /tmp/kia_hdrs | tr -d '\\r' | cut -d' ' -f2)\n\nmkdir -p \"$(dirname \"$KIA_SESSION\")\" && chmod 700 \"$(dirname \"$KIA_SESSION\")\"\njq -nc --arg r \"$RMTOKEN\" --arg d \"$KIA_DEVICE\" '{rmtoken:$r,deviceId:$d}' \\\n  > \"$KIA_SESSION\"\nchmod 600 \"$KIA_SESSION\"\n```\n\n`$KIA_SESSION` — **not** `session.json`. That neighbouring file is the MCP\nserver's own store (keyed by `accountId`, different schema); clobbering it sends\nthe server back through MFA.\n\n## 2. Refresh — no MFA, use this every other time\n\n```bash\nRMTOKEN=$(jq -r .rmtoken \"$KIA_SESSION\")\nKIA_DEVICE=$(jq -r .deviceId \"$KIA_SESSION\")\n\nkia_curl POST prof/authUser \"$(jq -nc \\\n  --arg u \"$KIA_USERNAME\" --arg p \"$KIA_PASSWORD\" --arg d \"$KIA_DEVICE\" \\\n  '{deviceKey:$d,deviceType:2,userCredential:{userId:$u,password:$p}}')\" \\\n  -- \"rmtoken: $RMTOKEN\" | jq '.status.statusCode'\n\nSID=$(grep -i '^sid:' /tmp/kia_hdrs | tr -d '\\r' | cut -d' ' -f2)\n```\n\nThe `rmtoken` is **not** rotated — the stored one keeps working.\n\n## 3. Reads\n\n```bash\n# Vehicle list -> vinkey\nkia_curl GET ownr/gvl '' -- \"sid: $SID\" | tee /tmp/kia_gvl.json \\\n  | jq '.payload.vehicleSummary[] | {nickName, modelYear, modelName, mileage, vehicleKey}'\nVIN=$(jq -r '.payload.vehicleSummary[0].vehicleKey' /tmp/kia_gvl.json)\n```\n\n```bash\n# Cached status. airTempRange/seatHeatCoolOption = \"1\" or the climate block is ABSENT.\nGVI=$(jq -nc --arg v \"$VIN\" '{\n  vehicleConfigReq:{airTempRange:\"1\",maintenance:\"1\",seatHeatCoolOption:\"1\",\n                    vehicle:\"1\",vehicleFeature:\"1\"},\n  vehicleInfoReq:{drivingActivty:\"0\",dtc:\"1\",enrollment:\"1\",functionalCards:\"0\",\n                  location:\"1\",vehicleStatus:\"1\",weather:\"0\"},\n  vinKey:[$v]}')\n\nkia_curl POST cmm/gvi \"$GVI\" -- \"sid: $SID\" \"vinkey: $VIN\" > /tmp/kia_gvi.json\n\n# The fields worth looking at\njq '.payload.vehicleInfoList[0].lastVehicleInfo.vehicleStatusRpt.vehicleStatus\n    | {doorLock, ign3, engine,\n       climate: {airCtrl: .climate.airCtrl, temp: .climate.airTemp.value},\n       battery: .evStatus.batteryStatus,\n       range:   .evStatus.drvDistance[0].rangeByFuel.totalAvailableRange.value,\n       synced:  .syncDate.utc}' /tmp/kia_gvi.json\n```\n\n`drivingActivty` is **misspelled in Kia's API**. Correcting it drops the field.\n\n```bash\n# Location\njq '.payload.vehicleInfoList[0].lastVehicleInfo.location\n    | {lat: .coord.lat, lon: .coord.lon, synced: .syncDate.utc}' /tmp/kia_gvi.json\n\n# Force a fresh read from the vehicle (slow — wakes the telematics unit)\nkia_curl POST rems/rvs '{\"requestType\":0}' -- \"sid: $SID\" \"vinkey: $VIN\" | jq '.status'\n\n# EV charge targets (AC and DC)\nkia_curl GET evc/gts '' -- \"sid: $SID\" \"vinkey: $VIN\" \\\n  | jq '.payload.targetSOClist[] | {plugType, targetSOClevel}'\n```\n\n## 4. Commands\n\n```bash\n# Snapshot the fields you intend to prove changed — NEVER include syncDate.\nkia_state() {\n  kia_curl POST cmm/gvi \"$GVI\" -- \"sid: $SID\" \"vinkey: $VIN\" \\\n    | jq -c '.payload.vehicleInfoList[0].lastVehicleInfo.vehicleStatusRpt.vehicleStatus\n             | {doorLock, ign3, airCtrl: .climate.airCtrl}'\n}\nBEFORE=$(kia_state); echo \"before: $BEFORE\"\n```\n\n```bash\n# Doors — VERIFIED\nkia_curl GET rems/door/lock   '' -- \"sid: $SID\" \"vinkey: $VIN\" | jq '.status.statusCode'\nkia_curl GET rems/door/unlock '' -- \"sid: $SID\" \"vinkey: $VIN\" | jq '.status.statusCode'\n\n# Climate on — VERIFIED. duration is minutes; temperature is best-effort (see below).\nkia_curl POST rems/start \"$(jq -nc '{remoteClimate:{\n    airTemp:{unit:1,value:\"70\"}, airCtrl:true, defrost:false,\n    heatingAccessory:{rearWindow:0,sideMirror:0,steeringWheel:0,steeringWheelStep:0},\n    ignitionOnDuration:{unit:4,value:10}}}')\" \\\n  -- \"sid: $SID\" \"vinkey: $VIN\" | jq '.status.statusCode'\n\n# Climate off — VERIFIED\nkia_curl GET rems/stop '' -- \"sid: $SID\" \"vinkey: $VIN\" | jq '.status.statusCode'\n```\n\nOmit `heatVentSeat` unless you know the car supports the seats you name — Kia\nvalidates seat capability per vehicle.\n\n```bash\n# Charging — VERIFIED against a plugged-in EV9.\n# Proof: vehicleStatus.evStatus.batteryCharge flips; evc/sts proven via evc/gts.\nkia_curl POST evc/charge '{\"chargeRatio\":100}' -- \"sid: $SID\" \"vinkey: $VIN\"\nkia_curl GET  evc/cancel ''                    -- \"sid: $SID\" \"vinkey: $VIN\"\n\n# Send BOTH plug types — evc/sts replaces the list, so omitting one drops it.\nkia_curl POST evc/sts '{\"targetSOClist\":[{\"plugType\":0,\"targetSOClevel\":90},\n                                          {\"plugType\":1,\"targetSOClevel\":80}]}' \\\n  -- \"sid: $SID\" \"vinkey: $VIN\"\n\n# Charging state, for proving the above\nkia_curl POST cmm/gvi \"$GVI\" -- \"sid: $SID\" \"vinkey: $VIN\" \\\n  | jq '.payload.vehicleInfoList[0].lastVehicleInfo.vehicleStatusRpt.vehicleStatus.evStatus\n        | {batteryCharge, batteryStatus, batteryPlugin}'\n```\n\n## 5. Prove it landed\n\n```bash\n# Poll the re-read. This — not cmm/gts — is the proof.\nfor i in $(seq 1 9); do\n  sleep 10\n  AFTER=$(kia_state)\n  [ \"$AFTER\" != \"$BEFORE\" ] && { echo \"changed: $BEFORE -> $AFTER\"; break; }\n  [ \"$i\" = 9 ] && echo \"NO observed change in ~90s: $AFTER\"\ndone\n```\n\n`plugType` 0 and 1 are the two charge connectors; `targetSOClevel` is a percentage.\n\n## Error quick-reference\n\n| errorCode | Meaning | Do |\n| --- | --- | --- |\n| `0` | success | — |\n| `9200` | missing mandatory header | regenerate `date`; it must be fresh RFC-1123 |\n| `1001` | invalid email or password | fix it; **do not retry in a loop** |\n| `1037` | invalid email address | check the address format/domain |\n\nSession expired (a previously-working `sid` starts failing) → re-run §2. It needs\nno MFA.\n\nFile v1.0.3:skill-card.md\n\n## Description:\n\nProvides curl-based guidance and request examples for querying Kia vehicle status, location, EV charge state, and remote commands through the Kia Owners API.\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 proficient vehicle owners use this skill to prepare one-off shell commands for reading Kia vehicle state and issuing remote lock, climate, and charging actions without running the related MCP server.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: The skill can help prepare commands that remotely affect a real Kia vehicle, including unlock, climate, and charging actions.\n\nMitigation: Require explicit user confirmation before running any vehicle-changing command and re-read vehicle state after execution to verify the intended result.\n\nRisk: The workflow stores an rmtoken that can refresh access to the vehicle account.\n\nMitigation: Store the token only in the documented session file with restrictive permissions, avoid sharing it, and delete it when access is no longer needed.\n\n## Reference(s):\n\n- [Ready-to-run requests](artifact/references/requests.md)\n- [Kia Owners API endpoint](https://api.owners.kia.com/apigw/v1)\n- [ClawHub skill page](https://clawhub.ai/chrischall/skills/kiaaccess-curl)\n\n## Skill Output:\n\n**Output Type(s):** [text, markdown, shell commands, configuration, guidance]\n\n**Output Format:** [Markdown with inline bash and jq command examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Includes request bodies, header setup, token storage guidance, and verification steps for vehicle actions.]\n\n## Skill Version(s):\n\n1.0.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.0.2: 4 files, 8650 bytes\n\nFiles: references/requests.md (9510b), skill-card.md (2585b), SKILL.md (4737b), _meta.json (133b)\n\nFile v1.0.2:SKILL.md\n\n---\nname: kiaaccess-curl\ndescription: Query and command a Kia vehicle directly with curl against the Kia Owners API (api.owners.kia.com), without running the MCP server. Use when the user wants a one-off read of their Kia's status, location, or EV charge state, or to lock/unlock/start climate from the shell — \"check my Kia\", \"is the car locked\", \"what's the EV9 charge\", \"lock the car from the terminal\". Requires KIA_USERNAME/KIA_PASSWORD and a one-time SMS/email MFA bootstrap.\n---\n\n# Kia Owners API via curl\n\nThe Kia Access app's API is reachable server-side — no browser, no bridge, no\nextension. This skill talks to it directly with `curl`.\n\nPrefer the `kiaaccess-mcp` server for anything conversational or repeated; use\nthis for one-off shell work, debugging, or when the server isn't running.\n\n**Ready-to-run request bodies and `jq` recipes: `references/requests.md`.**\nFull shape reference (verified live): `../../docs/KIA-API.md`.\n\n## Setup\n\n```bash\nexport KIA_USERNAME='you@example.com'\nexport KIA_PASSWORD='…'\nexport KIA_DEVICE=$(uuidgen)        # keep this stable across runs\n```\n\n## Two rules that will bite you\n\n1. **Every request needs an RFC-1123 `date` header.** Omit it and you get\n   `errorCode 9200 \"Missing mandatory data in header\"` — a message that does not\n   name the culprit. Regenerate it per request; a stale one is rejected.\n2. **HTTP is 200 even on failure.** Success is `status.statusCode == 0` in the\n   *body*. Never branch on the HTTP code.\n\nSource `references/requests.md`'s `kia_headers` helper rather than hand-rolling\nheaders — it handles both.\n\n## Auth: one-time MFA, then silent refresh\n\n`authUser` → `sendOTP` → `verifyOTP` yields a **`sid`** (session, short-lived)\nand an **`rmtoken`** (refresh, durable), both as *response headers*.\n\nAfterwards, `authUser` with the `rmtoken` header mints a fresh `sid` with **no\nMFA**. So you do the SMS dance once and then never again — save the `rmtoken`.\n\n> **Never retry a rejected login.** `errorCode 1001` (bad credentials) or `1037`\n> (bad email) increments `payload.loginAttempt`; enough failures set\n> `enforceRecaptcha` and **permanently break shell-based login**. Fix the\n> credential and try once.\n\nStore the `rmtoken` at `$KIA_SESSION` (default `~/.kiaaccess-mcp/curl-session.json`)\nwith `chmod 600`. It is a credential: it re-authenticates the account without a\npassword prompt.\n\n> Do **not** write it to `~/.kiaaccess-mcp/session.json`. That path belongs to the\n> `kiaaccess-mcp` server, whose store is keyed by `accountId` with a different\n> schema — overwriting it corrupts the server's session and forces it back\n> through MFA.\n\n## Calling\n\nReads and commands take `sid` (+ `vinkey` for vehicle-scoped calls). Get the\n`vinkey` from `ownr/gvl` → `payload.vehicleSummary[0].vehicleKey`.\n\n| Want | Endpoint |\n| --- | --- |\n| vehicles | `GET ownr/gvl` |\n| status (cached) | `POST cmm/gvi` |\n| status (force refresh) | `POST rems/rvs` |\n| EV charge targets | `GET evc/gts` |\n| lock / unlock | `GET rems/door/lock` / `rems/door/unlock` |\n| climate on / off | `POST rems/start` / `GET rems/stop` |\n| charge start / stop / limits | `POST evc/charge` / `GET evc/cancel` / `POST evc/sts` |\n\n## Confirming a command actually worked\n\n**A `statusCode: 0` means \"accepted\", not \"done\".**\n\n- **Do not poll `cmm/gts`.** Despite taking an `xid`, it returns global flags and\n  never reports per-action completion — polled through a real lock it never\n  changed.\n- **Re-read `cmm/gvi` and diff the field.** That is the only proof. Allow ~30–60s.\n\nFields to diff:\n\n| Command | Field |\n| --- | --- |\n| lock / unlock | `vehicleStatus.doorLock` |\n| climate on / off | `vehicleStatus.climate.airCtrl` and `vehicleStatus.ign3` |\n\nThree traps when writing that comparison:\n\n- **`syncDate` advances on every read.** Include it and *every* command looks\n  successful. Exclude it.\n- **There is no `airCtrlOn`.** Climate is nested under `vehicleStatus.climate`,\n  and the whole block is **absent** unless you request `cmm/gvi` with\n  `vehicleConfigReq.airTempRange: \"1\"` and `seatHeatCoolOption: \"1\"`.\n- **On an EV, `engine` stays `false`** with climate running — use `ign3`.\n\n## Verification status\n\nEvery endpoint here was verified live against a 2024 EV9 — all reads, the door\nand climate commands, and (against a plugged-in car) `evc/charge`, `evc/cancel`\nand `evc/sts`, each proven by a re-read rather than a 200.\n\nCharging proof fields live under `vehicleStatus.evStatus`: `batteryCharge`\n(true while charging), `batteryStatus` (SOC %), `batteryPlugin`. `evc/sts` is\nproven by re-reading `evc/gts`.\n\nOne caveat: **`rems/start`'s temperature may not apply** — a start requesting 70\nleft `airTemp.value` at 72. Treat it as best-effort.\n\nFile v1.0.2:_meta.json\n\n{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"kiaaccess-curl\",\n  \"version\": \"1.0.2\",\n  \"publishedAt\": 1790178179682\n}\n\nFile v1.0.2:references/requests.md\n\n# Ready-to-run requests\n\nEvery shape here was verified live on 2026-07-27 against a 2024 Kia EV9 —\nincluding the `evc/*` charging commands, run against the car while plugged in.\n\n## Header helper\n\nSource this first. It regenerates the mandatory `date` per call and keeps the\ndevice id stable.\n\n```bash\nKIA_BASE='https://api.owners.kia.com/apigw/v1'\n: \"${KIA_DEVICE:?export KIA_DEVICE=\\$(uuidgen) first}\"\n\n# Session file for THIS skill. Deliberately NOT ~/.kiaaccess-mcp/session.json —\n# that path belongs to the MCP server, whose store is keyed by accountId with a\n# different schema (src/session.ts). Writing this skill's flat\n# {rmtoken, deviceId} there corrupts the server's session and forces it back\n# through MFA.\nKIA_SESSION=\"${KIA_CURL_SESSION:-$HOME/.kiaaccess-mcp/curl-session.json}\"\n\n# Builds the header list into the KIA_HDRS array. Extra headers passed as args.\nkia_headers() {\n  local z sign off h\n  z=$(date +%z)                       # e.g. -0800, +0530\n  sign=${z:0:1}\n  # `10#` forces base 10. Without it, bash reads a leading-zero offset such as\n  # `08`/`09` as OCTAL and dies with \"value too great for base\" — so this breaks\n  # in US Pacific winter, Alaska, Japan, Korea and China. zsh does not have the\n  # problem, which is a good way to ship it broken without noticing.\n  off=$(( 10#${z:1:2} ))\n  [ \"$sign\" = \"-\" ] && off=$(( 0 - off ))\n\n  KIA_HDRS=()\n  for h in \\\n    \"content-type: application/json;charset=utf-8\" \\\n    \"accept: application/json\" \\\n    \"accept-language: en-US,en;q=0.9\" \\\n    \"accept-charset: utf-8\" \\\n    \"apptype: L\" \"appversion: 7.22.0\" \"clientid: SPACL716-APL\" \\\n    \"clientuuid: ${KIA_DEVICE}\" \"deviceid: ${KIA_DEVICE}\" \\\n    \"from: SPA\" \"host: api.owners.kia.com\" \"language: 0\" \\\n    \"offset: ${off}\" \"ostype: iOS\" \"osversion: 15.8.5\" \"phonebrand: iPhone\" \\\n    \"secretkey: sydnat-9kykci-Kuhtep-h5nK\" \"to: APIGW\" \"tokentype: A\" \\\n    \"date: $(LC_ALL=C date -u '+%a, %d %b %Y %H:%M:%S GMT')\" \\\n    \"user-agent: KIAPrimo_iOS/37 CFNetwork/1335.0.3.4 Darwin/21.6.0\" \\\n    \"$@\"\n  do\n    KIA_HDRS+=(-H \"$h\")\n  done\n}\n\n# curl wrapper: kia_curl <method> <path> [body] [-- extra-header ...]\nkia_curl() {\n  local method=\"$1\" path=\"$2\" body=\"${3:-}\"\n  shift 2; [ $# -gt 0 ] && shift          # drop the body arg when present\n  [ \"${1:-}\" = \"--\" ] && shift\n  kia_headers \"$@\"\n  curl -sS -X \"$method\" \"${KIA_BASE}/${path}\" \"${KIA_HDRS[@]}\" \\\n    ${body:+--data \"$body\"} -D /tmp/kia_hdrs --compressed\n}\n```\n\nBoth functions work under bash and zsh. **Test under `bash` if you change them** —\nthe octal trap above bites only bash, so zsh-only testing hides it. An array is\nused rather than piping headers through `sed`, which avoids depending on GNU\nsed's `\\n`-in-replacement behaviour.\n\n`secretkey` is a **static app constant**, not a user secret — it is the same for\nevery install.\n\n## 1. Login (one-time MFA)\n\n```bash\n# Step 1 — authenticate. Captures otpKey (body) and xid (RESPONSE HEADER).\nkia_curl POST prof/authUser \"$(jq -nc \\\n  --arg u \"$KIA_USERNAME\" --arg p \"$KIA_PASSWORD\" \\\n  '{deviceKey:\"\",deviceType:2,userCredential:{userId:$u,password:$p},tncFlag:1}')\" \\\n  | tee /tmp/kia_auth.json | jq '.status, .payload.nextAction'\n\nOTPKEY=$(jq -r '.payload.otpKey' /tmp/kia_auth.json)\nXID=$(grep -i '^xid:' /tmp/kia_hdrs | tr -d '\\r' | cut -d' ' -f2)\n```\n\n> Stop here if `status.errorCode` is `1001` or `1037`. Fix the credential — do\n> **not** loop. Repeated failures set `enforceRecaptcha` and permanently break\n> shell login.\n\n```bash\n# Step 2 — send the code. notifytype is SMS or EMAIL.\nkia_curl POST cmm/sendOTP '{}' -- \\\n  \"otpkey: $OTPKEY\" \"notifytype: SMS\" \"xid: $XID\" | jq '.status, .payload.message'\n\n# Step 3 — verify. sid + rmtoken come back as RESPONSE HEADERS.\nread -r -p 'code: ' CODE\nkia_curl POST cmm/verifyOTP \"$(jq -nc --arg o \"$CODE\" '{otp:$o}')\" -- \\\n  \"otpkey: $OTPKEY\" \"xid: $XID\" | jq '.status'\n\nSID=$(grep -i '^sid:'     /tmp/kia_hdrs | tr -d '\\r' | cut -d' ' -f2)\nRMTOKEN=$(grep -i '^rmtoken:' /tmp/kia_hdrs | tr -d '\\r' | cut -d' ' -f2)\n\nmkdir -p \"$(dirname \"$KIA_SESSION\")\" && chmod 700 \"$(dirname \"$KIA_SESSION\")\"\njq -nc --arg r \"$RMTOKEN\" --arg d \"$KIA_DEVICE\" '{rmtoken:$r,deviceId:$d}' \\\n  > \"$KIA_SESSION\"\nchmod 600 \"$KIA_SESSION\"\n```\n\n`$KIA_SESSION` — **not** `session.json`. That neighbouring file is the MCP\nserver's own store (keyed by `accountId`, different sche\n\nArchive v1.0.1: 4 files, 8463 bytes\n\nFiles: references/requests.md (9510b), skill-card.md (2278b), SKILL.md (4737b), _meta.json (133b)","readmeExcerpt":"Skill: kiaaccess-curl Owner: chrischall Summary: Query and command a Kia vehicle directly with curl against the Kia Owners API (api.owners.kia.com), without running the MCP server. Use when the user wants a one-off read of their Kia's status, location, or EV charge state, or to lock/unlock/start climate from the shell — \"check my Kia\", \"is the car locked\", \"what's the EV9 charge\", \"lock the car from the terminal\". Re","codeSnippets":[],"executableExamples":[{"language":"bash","snippet":"export KIA_USERNAME='you@example.com'\nexport KIA_PASSWORD='…'\nexport KIA_DEVICE=$(uuidgen)        # keep this stable across runs"},{"language":"bash","snippet":"KIA_BASE='https://api.owners.kia.com/apigw/v1'\n: \"${KIA_DEVICE:?export KIA_DEVICE=\\$(uuidgen) first}\"\n\n# Session file for THIS skill. Deliberately NOT ~/.kiaaccess-mcp/session.json —\n# that path belongs to the MCP server, whose store is keyed by accountId with a\n# different schema (src/session.ts). Writing this skill's flat\n# {rmtoken, deviceId} there corrupts the server's session and forces it back\n# through MFA.\nKIA_SESSION=\"${KIA_CURL_SESSION:-$HOME/.kiaaccess-mcp/curl-session.json}\"\n\n# Builds the header list into the KIA_HDRS array. Extra headers passed as args.\nkia_headers() {\n  local z sign off h\n  z=$(date +%z)                       # e.g. -0800, +0530\n  sign=${z:0:1}\n  # `10#` forces base 10. Without it, bash reads a leading-zero offset such as\n  # `08`/`09` as OCTAL and dies with \"value too great for base\" — so this breaks\n  # in US Pacific winter, Alaska, Japan, Korea and China. zsh does not have the\n  # problem, which is a good way to ship it broken without noticing.\n  off=$(( 10#${z:1:2} ))\n  [ \"$sign\" = \"-\" ] && off=$(( 0 - off ))\n\n  KIA_HDRS=()\n  for h in \\\n    \"content-type: application/json;charset=utf-8\" \\\n    \"accept: application/json\" \\\n    \"accept-language: en-US,en;q=0.9\" \\\n    \"accept-charset: utf-8\" \\\n    \"apptype: L\" \"appversion: 7.22.0\" \"clientid: SPACL716-APL\" \\\n    \"clientuuid: ${KIA_DEVICE}\" \"deviceid: ${KIA_DEVICE}\" \\\n    \"from: SPA\" \"host: api.owners.kia.com\" \"language: 0\" \\\n    \"offset: ${off}\" \"ostype: iOS\" \"osversion: 15.8.5\" \"phonebrand: iPhone\" \\\n    \"secretkey: sydnat-9kykci-Kuhtep-h5nK\" \"to: APIGW\" \"tokentype: A\" \\\n    \"date: $(LC_ALL=C date -u '+%a, %d %b %Y %H:%M:%S GMT')\" \\\n    \"user-agent: KIAPrimo_iOS/37 CFNetwork/1335.0.3.4 Darwin/21.6.0\" \\\n    \"$@\"\n  do\n    KIA_HDRS+=(-H \"$h\")\n  done\n}\n\n# curl wrapper: kia_curl <method> <path> [body] [-- extra-header ...]\nkia_curl() {\n  local method=\"$1\" path=\"$2\" body=\"${3:-}\"\n  shift 2; [ $# -gt 0 ] && shift          # drop the body arg when present\n  [ \"${1:-}\" = \"--\" ] && shift\n  kia_h"},{"language":"bash","snippet":"# Step 1 — authenticate. Captures otpKey (body) and xid (RESPONSE HEADER).\nkia_curl POST prof/authUser \"$(jq -nc \\\n  --arg u \"$KIA_USERNAME\" --arg p \"$KIA_PASSWORD\" \\\n  '{deviceKey:\"\",deviceType:2,userCredential:{userId:$u,password:$p},tncFlag:1}')\" \\\n  | tee /tmp/kia_auth.json | jq '.status, .payload.nextAction'\n\nOTPKEY=$(jq -r '.payload.otpKey' /tmp/kia_auth.json)\nXID=$(grep -i '^xid:' /tmp/kia_hdrs | tr -d '\\r' | cut -d' ' -f2)"},{"language":"bash","snippet":"# Step 2 — send the code. notifytype is SMS or EMAIL.\nkia_curl POST cmm/sendOTP '{}' -- \\\n  \"otpkey: $OTPKEY\" \"notifytype: SMS\" \"xid: $XID\" | jq '.status, .payload.message'\n\n# Step 3 — verify. sid + rmtoken come back as RESPONSE HEADERS.\nread -r -p 'code: ' CODE\nkia_curl POST cmm/verifyOTP \"$(jq -nc --arg o \"$CODE\" '{otp:$o}')\" -- \\\n  \"otpkey: $OTPKEY\" \"xid: $XID\" | jq '.status'\n\nSID=$(grep -i '^sid:'     /tmp/kia_hdrs | tr -d '\\r' | cut -d' ' -f2)\nRMTOKEN=$(grep -i '^rmtoken:' /tmp/kia_hdrs | tr -d '\\r' | cut -d' ' -f2)\n\nmkdir -p \"$(dirname \"$KIA_SESSION\")\" && chmod 700 \"$(dirname \"$KIA_SESSION\")\"\njq -nc --arg r \"$RMTOKEN\" --arg d \"$KIA_DEVICE\" '{rmtoken:$r,deviceId:$d}' \\\n  > \"$KIA_SESSION\"\nchmod 600 \"$KIA_SESSION\""},{"language":"bash","snippet":"RMTOKEN=$(jq -r .rmtoken \"$KIA_SESSION\")\nKIA_DEVICE=$(jq -r .deviceId \"$KIA_SESSION\")\n\nkia_curl POST prof/authUser \"$(jq -nc \\\n  --arg u \"$KIA_USERNAME\" --arg p \"$KIA_PASSWORD\" --arg d \"$KIA_DEVICE\" \\\n  '{deviceKey:$d,deviceType:2,userCredential:{userId:$u,password:$p}}')\" \\\n  -- \"rmtoken: $RMTOKEN\" | jq '.status.statusCode'\n\nSID=$(grep -i '^sid:' /tmp/kia_hdrs | tr -d '\\r' | cut -d' ' -f2)"},{"language":"bash","snippet":"# Vehicle list -> vinkey\nkia_curl GET ownr/gvl '' -- \"sid: $SID\" | tee /tmp/kia_gvl.json \\\n  | jq '.payload.vehicleSummary[] | {nickName, modelYear, modelName, mileage, vehicleKey}'\nVIN=$(jq -r '.payload.vehicleSummary[0].vehicleKey' /tmp/kia_gvl.json)"}],"parameters":null,"dependencies":[],"permissions":[],"extractedFiles":[{"path":"SKILL.md","content":"---\nname: kiaaccess-curl\ndescription: Query and command a Kia vehicle directly with curl against the Kia Owners API (api.owners.kia.com), without running the MCP server. Use when the user wants a one-off read of their Kia's status, location, or EV charge state, or to lock/unlock/start climate from the shell — \"check my Kia\", \"is the car locked\", \"what's the EV9 charge\", \"lock the car from the terminal\". Requires KIA_USERNAME/KIA_PASSWORD and a one-time SMS/email MFA bootstrap.\n---\n\n# Kia Owners API via curl\n\nThe Kia Access app's API is reachable server-side — no browser, no bridge, no\nextension. This skill talks to it directly with `curl`.\n\nPrefer the `kiaaccess-mcp` server for anything conversational or repeated; use\nthis for one-off shell work, debugging, or when the server isn't running.\n\n**Ready-to-run request bodies and `jq` recipes: `references/requests.md`.**\nFull shape reference (verified live): `../../docs/KIA-API.md`.\n\n## Setup\n\n```bash\nexport KIA_USERNAME='you@example.com'\nexport KIA_PASSWORD='…'\nexport KIA_DEVICE=$(uuidgen)        # keep this stable across runs\n```\n\n## Two rules that will bite you\n\n1. **Every request needs an RFC-1123 `date` header.** Omit it and you get\n   `errorCode 9200 \"Missing mandatory data in header\"` — a message that does not\n   name the culprit. Regenerate it per request; a stale one is rejected.\n2. **HTTP is 200 even on failure.** Success is `status.statusCode == 0` in the\n   *body*. Never branch on the HTTP code.\n\nSource `references/requests.md`'s `kia_headers` helper rather than hand-rolling\nheaders — it handles both.\n\n## Auth: one-time MFA, then silent refresh\n\n`authUser` → `sendOTP` → `verifyOTP` yields a **`sid`** (session, short-lived)\nand an **`rmtoken`** (refresh, durable), both as *response headers*.\n\nAfterwards, `authUser` with the `rmtoken` header mints a fresh `sid` with **no\nMFA**. So you do the SMS dance once and then never again — save the `rmtoken`.\n\n> **Never retry a rejected login.** `errorCode 1001` (bad credentials) or `1037`\n> (bad email) increments `payload.loginAttempt`; enough failures set\n> `enforceRecaptcha` and **permanently break shell-based login**. Fix the\n> credential and try once.\n\nStore the `rmtoken` at `$KIA_SESSION` (default `~/.kiaaccess-mcp/curl-session.json`)\nwith `chmod 600`. It is a credential: it re-authenticates the account without a\npassword prompt.\n\n> Do **not** write it to `~/.kiaaccess-mcp/session.json`. That path belongs to the\n> `kiaaccess-mcp` server, whose store is keyed by `accountId` with a different\n> schema — overwriting it corrupts the server's session and forces it back\n> through MFA.\n\n## Calling\n\nReads and commands take `sid` (+ `vinkey` for vehicle-scoped calls). Get the\n`vinkey` from `ownr/gvl` → `payload.vehicleSummary[0].vehicleKey`.\n\n| Want | Endpoint |\n| --- | --- |\n| vehicles | `GET ownr/gvl` |\n| status (cached) | `POST cmm/gvi` |\n| status (force refresh) | `POST rems/rvs` |\n| EV charge targets | `GET evc/gts` |\n| lock / unlock | `GET rems/door/"},{"path":"_meta.json","content":"{\n  \"ownerId\": \"kn700jq4sjtf2anb0rk3ft4p7n856872\",\n  \"slug\": \"kiaaccess-curl\",\n  \"version\": \"2.0.0\",\n  \"publishedAt\": 1791588243714\n}"},{"path":"references/requests.md","content":"# Ready-to-run requests\n\nEvery shape here was verified live on 2026-07-27 against a 2024 Kia EV9 —\nincluding the `evc/*` charging commands, run against the car while plugged in.\n\n## Header helper\n\nSource this first. It regenerates the mandatory `date` per call and keeps the\ndevice id stable.\n\n```bash\nKIA_BASE='https://api.owners.kia.com/apigw/v1'\n: \"${KIA_DEVICE:?export KIA_DEVICE=\\$(uuidgen) first}\"\n\n# Session file for THIS skill. Deliberately NOT ~/.kiaaccess-mcp/session.json —\n# that path belongs to the MCP server, whose store is keyed by accountId with a\n# different schema (src/session.ts). Writing this skill's flat\n# {rmtoken, deviceId} there corrupts the server's session and forces it back\n# through MFA.\nKIA_SESSION=\"${KIA_CURL_SESSION:-$HOME/.kiaaccess-mcp/curl-session.json}\"\n\n# Builds the header list into the KIA_HDRS array. Extra headers passed as args.\nkia_headers() {\n  local z sign off h\n  z=$(date +%z)                       # e.g. -0800, +0530\n  sign=${z:0:1}\n  # `10#` forces base 10. Without it, bash reads a leading-zero offset such as\n  # `08`/`09` as OCTAL and dies with \"value too great for base\" — so this breaks\n  # in US Pacific winter, Alaska, Japan, Korea and China. zsh does not have the\n  # problem, which is a good way to ship it broken without noticing.\n  off=$(( 10#${z:1:2} ))\n  [ \"$sign\" = \"-\" ] && off=$(( 0 - off ))\n\n  KIA_HDRS=()\n  for h in \\\n    \"content-type: application/json;charset=utf-8\" \\\n    \"accept: application/json\" \\\n    \"accept-language: en-US,en;q=0.9\" \\\n    \"accept-charset: utf-8\" \\\n    \"apptype: L\" \"appversion: 7.22.0\" \"clientid: SPACL716-APL\" \\\n    \"clientuuid: ${KIA_DEVICE}\" \"deviceid: ${KIA_DEVICE}\" \\\n    \"from: SPA\" \"host: api.owners.kia.com\" \"language: 0\" \\\n    \"offset: ${off}\" \"ostype: iOS\" \"osversion: 15.8.5\" \"phonebrand: iPhone\" \\\n    \"secretkey: sydnat-9kykci-Kuhtep-h5nK\" \"to: APIGW\" \"tokentype: A\" \\\n    \"date: $(LC_ALL=C date -u '+%a, %d %b %Y %H:%M:%S GMT')\" \\\n    \"user-agent: KIAPrimo_iOS/37 CFNetwork/1335.0.3.4 Darwin/21.6.0\" \\\n    \"$@\"\n  do\n    KIA_HDRS+=(-H \"$h\")\n  done\n}\n\n# curl wrapper: kia_curl <method> <path> [body] [-- extra-header ...]\nkia_curl() {\n  local method=\"$1\" path=\"$2\" body=\"${3:-}\"\n  shift 2; [ $# -gt 0 ] && shift          # drop the body arg when present\n  [ \"${1:-}\" = \"--\" ] && shift\n  kia_headers \"$@\"\n  curl -sS -X \"$method\" \"${KIA_BASE}/${path}\" \"${KIA_HDRS[@]}\" \\\n    ${body:+--data \"$body\"} -D /tmp/kia_hdrs --compressed\n}\n```\n\nBoth functions work under bash and zsh. **Test under `bash` if you change them** —\nthe octal trap above bites only bash, so zsh-only testing hides it. An array is\nused rather than piping headers through `sed`, which avoids depending on GNU\nsed's `\\n`-in-replacement behaviour.\n\n`secretkey` is a **static app constant**, not a user secret — it is the same for\nevery install.\n\n## 1. Login (one-time MFA)\n\n```bash\n# Step 1 — authenticate. Captures otpKey (body) and xid (RESPONSE HEADER).\nkia_curl POST prof/authUser \"$(jq -nc \\\n  --arg u \"$KIA_USERNAME\" --ar"},{"path":"skill-card.md","content":"## Description:\n\nGuides one-off Kia vehicle status checks and remote commands through shell requests to the Kia Owners API.\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\nKia owners and their authorized assistants use the skill to inspect vehicle status, location, and charging information or request door, climate, and charging actions from the shell.\n\n### Deployment Geography for Use:\n\nGlobal\n\n## Known Risks and Mitigations:\n\nRisk: The skill can access sensitive account and vehicle location data and issue real vehicle commands.\n\nMitigation: Run only on a private machine and require explicit confirmation before unlock, climate, or charging actions.\n\nRisk: Durable session credentials and temporary response files can expose account or vehicle data.\n\nMitigation: Protect the saved session file, restrict access to temporary files, and delete them when finished.\n\n## Reference(s):\n\n- [ClawHub skill release](https://clawhub.ai/chrischall/skills/kiaaccess-curl)\n- [Ready-to-run requests](references/requests.md)\n\n## Skill Output:\n\n**Output Type(s):** [Guidance, Shell commands]\n\n**Output Format:** [Markdown with bash examples]\n\n**Output Parameters:** [1D]\n\n**Other Properties Related to Output:** [Requires account credentials and one-time MFA setup; command acceptance does not prove a vehicle state change.]\n\n## Skill Version(s):\n\n2.0.0 (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":"Query and command a Kia vehicle directly with curl against the Kia Owners API (api.owners.kia.com), without running the MCP server. Use when the user wants a one-off read of their Kia's status, location, or EV charge state, or to lock/unlock/start climate from the shell — \"check my Kia\", \"is the car locked\", \"what's the EV9 charge\", \"lock the car from the terminal\". Requires KIA_USERNAME/KIA_PASSWORD and a one-time SMS/email MFA bootstrap. Skill: kiaaccess-curl Owner: chrischall Summary: Query and command a Kia vehicle directly with curl against the Kia Owners API (api.owners.kia.com), without running the MCP server. Use when the user wants a one-off read of their Kia's status, location, or EV charge state, or to lock/unlock/start climate from the shell — \"check my Kia\", \"is the car locked\", \"what's the EV9 charge\", \"lock the car from the terminal\". Re","editorialQuality":{"score":100,"threshold":65,"status":"ready","wordCount":1382,"uniquenessScore":45,"reasons":[]}},"media":{"evidence":{"source":"no-media","verified":false,"confidence":"low","updatedAt":"2026-10-10T02:51:24.503Z","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-10T02:51:24.503Z","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-10T08:45:32.271Z","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"}]}}}