Repository navigation
Tags
Tags
fix(migrate): address findings from the pre-release smoke test (#604) * fix(migrate): address findings from the pre-release smoke test - Report users created without an extra or unverified email or phone Clerk refused once the user existed: the summary lists them, `--json` adds them to `result.warnings`, the user's line records `refused`, and `runs <id>` shows them under warnings. Before, only users.ndjson said so. - A secret key Clerk refuses (401) stops the import with Clerk's error, rather than a stand-in instance that sent the run checks off course. A run undone in full no longer blocks a stand-in import with "did not confirm which instance". - Replace a gateway's HTML error page with "Clerk returned HTTP <status> with no error details" in import, undo and runs breakdowns. - A refused dry run says so after the checks, with the --allow-partial command (`refused: true` in --json). - Explain the APP_ID placeholder in fix commands (`checks.placeholders` in --json). - `--json` carries the notes the human summary adds to Clerk's messages, such as the development-instance user limit (`result.notes`). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(migrate): only call an attach refused when Clerk refused the identifier A 401 says the key stopped working, and an exhausted 429 says nothing about the email or phone, so neither is recorded as `refused` or warned about. Also lists `refused` among the users.ndjson fields, splits the `runs <id>` README sentence, and rewrites the changeset in short sentences. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(migrate): leave an attach pending when the failure isn't about the identifier A 401 or a 429 that outlasted its retries says nothing about the email or phone, so the attach stays pending and a re-run finishes it, rather than being dropped with only a note. Also types `createUser`'s response and adds the Oxford commas the README and changeset were missing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(migrate): restore createUser's untyped response, which tsc needs Typing it made tsc report a use before assignment on a path it can't follow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(migrate): leave an attach pending on a 403 that isn't about the phone A 403 can mean the key isn't permitted, which says nothing about the email or phone, so the attach stays pending for a re-run. A country Clerk doesn't support (`unsupported_country_code`) is still a refusal of the phone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(migrate): report dropped phones in runs <id>, and match the refused dry run to the real one A first phone Clerk refused on the create now lands on the user's line as `phoneRefused`, so `runs <id>` warns about it the way the import summary does. A refused dry run now gives the real refusal's advice and next steps, including the earlier run's undo or continue. The README also says which attach errors stay pending. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(migrate): address Roy's second review of the final-testing fixes - An attach Clerk answers `form_identifier_exists` looks the user up first: when the user already holds it, an earlier attach with no answer landed, so it counts as attached, not refused. A failed lookup leaves it pending. - A refused first phone's reason goes on the retry's `creating` line too, so a continue that adopts the user keeps `phoneRefused` and its note. - A created user's first line carries its notes, so a stop before the second line doesn't lose why the phone was dropped. - A refused `--dry-run --json` carries the real refusal's `advice` and `steps`. - `runs <id>` lists those users under "Imported without an email or phone", and both commands share the warning prefixes. - Tests for a 429 attach that outlasts the retries, the taken-by-this-user lookup, the adopted phone refusal, and the SMS note on a refused attach. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(migrate): address Roy's third review of the final-testing fixes - An email the instance's allowlist or blocklist refuses (`not_allowed_access`) is a refusal, not a key problem left pending. - A taken phone counts as attached when the user holds it in E.164, whatever the source wrote; emails are trimmed. - A lone reserved phone a retry already counts as refused isn't also pending, so a continue that adopts the user warns about it once. - An attach left pending on a Clerk error notes why, once across continues, and `runs <id>` lists those users. - `runs <id>` drops its warnings once the run is undone in full. - A refused dry run prints its next steps in agent mode too, and a real refusal's `--json` carries `advice` and `steps`. - A key Clerk rejects names where it came from, with Clerk's long message. - Tests for the continue's phone wiring, the dry run's next steps, and a 403 for the country; README and changeset brought up to date. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(migrate): group refusals without the identifier, and word an HTML 401 by status - A refusal's summary key drops the email or phone Clerk's message names, so `not_allowed_access` refusals group as one warning instead of one per user. - A 401 whose body is a proxy's HTML page reads "Clerk returned HTTP 401 with no error details" instead of the markup. - Spread the modes into `test.each`, per the testing rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor(migrate): address Roy's nits on the final-testing fixes - One `adoptedLines` map of `creating` lines replaces the `adoptedReserved`, `adoptedPending` and `adoptedPhoneRefused` options. - `RegExp.escape`, no spread for an unset body, notes and the key source each built once, and a one-line warning loop without a status check that never skipped anything. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Bump. --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Use the canonical audit-clerk-skill in clerk/skills (#540) * Point CLI audit guidance to canonical skills repo * Keep CLI audit skill as pointer to canonical guidance * Resolve skills checkout before CLI skill audit * docs: resolve canonical audit skill from local checkout * Apply batched suggestions from code review Co-authored-by: Michael Novotny <manovotny@gmail.com> --------- Co-authored-by: Michael Novotny <manovotny@gmail.com>
Dispatch add-auth evals after stable Clerk CLI releases (#539) * Dispatch add-auth evals after stable CLI releases * Dispatch CLI release evals without waiting for npm latest * Clarify stable CLI release dispatch and alerting * Document CLI release dispatch usage
docs(init): drop references to the old categorized clerk/skills layout ( #601) * docs(init): drop references to the old categorized clerk/skills layout clerk/skills now keeps every skill at skills/<name>/, so the cli/, core/, features/, and mobile/ directory names in the init README and skills.ts comments no longer exist. Skill names and install behavior are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(init): drop the old skill grouping from the init steps Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
fix(init): stop requesting the retired clerk-expo-patterns skill on E… …xpo (#602) * fix(init): stop requesting the retired clerk-expo-patterns skill on Expo clerk/skills 0d7ec1b folded clerk-expo-patterns into clerk-expo and removed the directory. `skills add` silently skips a name it can't find, so Expo installs still succeeded, but the prompt advertised a skill that never got installed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(init): cover the Expo prompt and drop the retired skill from the multi-skill case Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
feat(migrate): opt in to creating unverified identifiers reserved (#531) * feat(migrate): opt in to creating unverified identifiers reserved An email or phone the source never verified is attached unverified: the user cannot sign in with it, and another user can claim it. That stays the default. `--reserve-unverified`, or a yes at the prompt a human gets when the file has any, creates them reserved on POST /v1/users instead: usable for sign-in and locked to the user. `-y`, `--json` and agent mode never ask. A reserved identifier also meets an email or phone requirement, so the checks let those users through. Both halves are checked live in the E2E suite against the test app. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(migrate): attach what an adopted user's create left out, in its own mode A continued run adopts a user whose earlier create got no answer, and only attaches the rest. Which unverified identifiers were left came from this run's --reserve-unverified, not the earlier create's, so switching the flag lost them or attached duplicates. The creating line now records a reserved create, and an adopted user follows it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(migrate): keep prompt answers in the commands a run suggests A yes to the reserve or legal-checks prompt only set a local, so the --allow-partial command printed on a refusal left the flag out, and running it would reject the users that answer let through. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(migrate): offer no email fix that --reserve-unverified already covers With reservation, a user whose only email is unverified meets an email requirement, but the checks still offered to make email optional. A user with no email at all still gets that fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(migrate): keep the verified phone when Clerk refuses a reserved one With --reserve-unverified, `phone_number` carries the verified phone and the reserved ones. When Clerk refused any of them, the retry dropped the whole array, so the verified phone went too, and a user with no email failed outright. The retry now sends the verified phone alone first, and drops every phone only if Clerk refuses that as well. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(migrate): check reserved identifiers for duplicates too With --reserve-unverified, the unverified emails and phones go on the create, so a clash on one fails the whole create. The duplicate checks matched only the primary email and phone, so the dry run passed users the import would then fail. Both checks, in the file and in the instance, now match what the create sends, reserved identifiers included, and only when they are reserved. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(migrate): count an adopted user's reserved email toward a required one A continue without --reserve-unverified checked every user as if nothing were reserved. An adopted user whose cut-off create had reserved its only email (the `creating` line says `reserved: true`) was rejected for a missing required email, and since adopted users are not recorded as rejects, it stayed unfinished. The checks now treat those users as reserved, whatever this run's flag says. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(migrate): check an adopted user's identifiers in its own create's mode On a continue with --reserve-unverified, the checks treated every user's unverified identifiers as reserved, adopted users included. An adopted user whose cut-off create did not reserve them was then rejected for sharing one with an earlier record, though it would only be attached after. The checks now take an adopted user's mode from its `creating` line, and the flag only for everyone else, in the duplicate checks and the required-identifier check alike. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(migrate): count a reserved email or phone as a way in past a disabled provider Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(migrate): attach the reserved phones a refusal kept off the create one by one Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(migrate): name the phone endpoint too, and say an adopted user keeps its recorded mode Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(migrate): count no email Clerk refuses as a way in for a user on a disabled provider canSignInOtherwise read the user's emails before dropRefusedEmails, so an email the create would strip (an unroutable domain, say) kept a Supabase user whose only provider is off. It now reads what the create sends. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(migrate): keep a run's reserve choice for its continue, and say a reserved collision rejects the user The reserve choice came only from the flag or the prompt each time, so a run a human answered yes for, continued under -y, --json or in agent mode, created the rest unverified. A run now records reserveUnverified; a continue reuses it without asking and refuses --reserve-unverified on a run that kept them unverified. A run from before this asks as it did. Reserved, an unverified identifier is checked for duplicates, so one another user holds rejects the whole user. The prompt and the README now say so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(migrate): attach the reserved phones a cut-off retry left out when a continue adopts the user Every create attempt wrote `creating` with `reserved: true`, including the retry after a refused phone that sends without the reserved phones. Cut off before its answer, a continue adopted the user as reserved and never attached those phones. Each attempt's `creating` line now lists the reserved phones its body left out as `pending`, and a continue that adopts the user attaches them, unverified; the reserved emails aren't sent again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(migrate): drop loose phones before counting a way in past a disabled provider, and record a legacy run's reservation answer - With numeric usernames on, a phone not in E.164 form counted as a way in for a Supabase user on a disabled provider, though the create strips it. The user was then created with no way to sign in. The sign-in check now strips those phones first, as the create does. - A continue of a run recorded before the reservation choice asked, but never wrote the answer, so a later `-y` continue created the rest unverified. The continued record now carries it. - README: what counts as a way in, the duplicate check's "passes the checks", a legacy run asking once, and a pending phone Clerk refuses again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(migrate): continue an earlier run in its own reservation mode from a --new-run refusal The refusal's "Drop --new-run" command took --reserve-unverified from the new run's options. A continue of a run that kept its users unverified refuses that flag, so the command failed as printed. It now uses the earlier run's recorded mode. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(migrate): keep reserved and left-out phones on a create that got no answer With --reserve-unverified, a create that got a network error or 5xx wrote a later `creating` line without `reserved` or `pending`. A continue reads only the latest line, so it adopted the user as unreserved, sent the email Clerk already held reserved, and never attached the phones the retried create had left out. The unknown-outcome line now keeps both from the attempt. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(migrate): name --reserve-unverified in an unverified-only reject A user whose only email or phone is unverified is rejected where the instance requires one, and the fixes offered only making it optional. `--reserve-unverified` imports them, but only the prompt said so, and `--json` and agents never see it. The reject reason now names the flag and its tradeoff, as the legal-acceptance reject names --skip-legal-checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(migrate): say a reserved identifier's source never verified it The unverified-only reject said --reserve-unverified imports the address "though never verified", but a reserved identifier becomes verified the first time the user signs in with it. It now says the source never verified it, which is the tradeoff. The E2E test still asserted the reason from before --reserve-unverified was named in it, and failed on every run; it now asserts this one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(migrate): say a reserved identifier is verified by a code or link The README said a reserved identifier becomes verified the first time the user signs in with it. Clerk promotes it only when the sign-in's first factor proves ownership, a code or link sent to it; signing in with a password, using the address as the identifier, leaves it reserved. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(migrate): split the reserved-duplicate sentence in two The README put both outcomes of a duplicate reserved identifier in one sentence. It now says them separately: one another user holds rejects the whole user, and without reserving only that address fails to attach. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(migrate): say production instances have no user limit The README said production instances have no user limit "by default", and that an older API leaves a production import unchecked until the first refusal. A comment in run.ts said a plan can impose one. Clerk sets `max_allowed_users` to null on every production instance, created or cloned, and refuses to set one there, so only a development instance has a limit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(migrate): don't suggest --reserve-unverified to a continue that refuses it A continue of a run that recorded `reserveUnverified: false` takes the run's mode, so its checks named --reserve-unverified in the unverified-only reject reasons, and adding the flag then exited 2 ("Continue it without --reserve-unverified, or pass --new-run"). On such a continue the email and phone reasons now leave the hint out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
-
Nov 3, 2026 - ea2cecc
- zip
- tar.gz
- 8000 Notes
- Downloads
feat(migrate): record the source, stage, and target app and instance … …in telemetry (#596) * feat(migrate): record the source, funnel stage, and target app and instance in telemetry Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(migrate): cover telemetry for runs <id>, a prompted source, and a declined import Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(readme): name the migrate values telemetry takes from options Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(telemetry): send a null app for a key_ stand-in, not the linked one When `GET /v1/instance` failed on an import keyed by `--secret-key` or `CLERK_SECRET_KEY`, `setTelemetryTarget` dropped the `key_` stand-in, the target ended up empty, and the event fell back to the linked profile's app and workspace: an app the run may never have touched. A stand-in now counts as a target that's set but unnamed, so `app_id` and `workspace_id` go out as null, as the README says. An `--instance` alias still reports the linked app. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(telemetry): check migrate_source against the source registries `migrate_source` was checked against a list in `lib/telemetry.ts` that nothing tied to the registries, so a new built-in source or export platform would have reported as `custom`. `recordMigrateSource` now checks `sourceKeys()` and `exportPlatformKeys()` instead, in `commands/migrate/telemetry.ts` so `lib/telemetry.ts` doesn't load every source on every command. It stays the only caller of `setTelemetryMigrateSource`, so the payload still only sees a shipped source or `custom`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(telemetry): mark clerk migrate's requests in the User-Agent `clerk migrate`'s Backend API requests looked like any other CLI request in the `User-Agent`, so the logs couldn't tell a migration's user creates from `clerk users create`. The migrate group now marks its requests, and the `User-Agent` gains a `migrate` segment. Like `AIAgent`, the segment is for analytics, so the fetch layer leaves it out under the telemetry opt-outs and until the disclosure notice has shown. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(migrate): say which telemetry fields migrate adds and when each is null The README said migrate events carry "plus four" fields, though `app_id` and `stage` go out on every command, and that each is null when the run never learned it, though `app_id` falls back to the linked project. It now says migrate adds `migrate_source` and `instance_id` and gives `app_id` and `stage` values of its own, with the null cases in each row. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(migrate): put the telemetry exclusions and stages in the active voice Applies the review's wording for the two passive sentences, with an Oxford comma added to the list of values telemetry never sends. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(readme): drop the repeated telemetry exceptions and the passive voice Applies the review's wording for the root README's telemetry paragraph: the exceptions sentence no longer repeats the fields the first sentence lists, and `clerk feedback` scrubs the text itself. Oxford commas added to its two lists. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(changeset): split the migrate telemetry changeset's long sentence Applies the review's suggestion as written. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(migrate): say the migrate User-Agent segment skips the notice in CI The README said the CLI leaves the `migrate` segment out until the telemetry notice has shown. Like the events and the `AIAgent` segment, CI is exempt, so the segment goes out on a CI's first run. Applies the review's wording. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(migrate): say instance_id carries the --instance hint before the run resolves The migrate group records `--app` and `--instance` as a telemetry target before anything resolves, so a run that stops early still names what it was pointed at. The README said `instance_id` is null in that case. It now says a run that hasn't resolved its target sends the `ins_…` or `app_…` you passed, unchecked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(telemetry): leave the analytics User-Agent segments out of dev builds Usage events skip dev builds, but the gate for the `AIAgent` and `migrate` User-Agent segments didn't, so our own testing with a dev build would count as migrations in the Backend API logs. The gate now skips dev builds too, unless `CLERK_TELEMETRY_URL` lifts that, as it does for the events. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(migrate): say every request a migrate run sends carries the segment The flag lives in `loggedFetch` for the whole process, so Platform API, Frontend API, and export requests carry `migrate` too, as well as the Backend API requests the docs named. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(migrate): send libsql requests through loggedFetch The libsql client called `fetch` directly, the last library call to do so. Its requests skipped the `--verbose` log, Ctrl-C's abort, and the `User-Agent`, so the README's "the requests it sends" missed them. It now goes through `loggedFetch` like every other request. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(migrate): check --output before recording the export stage The export's preAction hook recorded the platform and the `migrate_export_read` stage before it checked `--output`. A bad extension then exited with a usage error, and telemetry reported a run that had reached credentials and reading when nothing was read. The hook now checks first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(telemetry): send the migrate segment only to the Backend API `markMigrateRequests()` marked every request for the rest of the run, so `; migrate` also went to Auth0, Google, WorkOS, Turso, and the npm registry. None of them reach Clerk's logs, the only reason the segment exists, and it told each of them about the migration. `loggedFetch` now adds it only to requests tagged `bapi`, and builds the User-Agent per request, since the segment depends on the tag. The README and changeset say "Backend API requests" again. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
feat(migrate): check imports against the user limit the instance repo… …rts (#552) * feat(migrate): check imports against the user limit the instance reports `GET /v1/instance` now returns `max_allowed_users` (clerk/clerk_go#22871), the limit Clerk enforces when creating a user, or `null` for none. The import's checks hold it on any instance instead of guessing 100 or reading CLERK_MIGRATE_DEV_USER_LIMIT, so a raised development instance no longer rejects users it has room for. An API without the field keeps today's fallback. `checks.quota.limitSource` says where the limit came from, and a reported limit's reject points at support rather than the variable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(migrate): point production limit rejects at a paid plan Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
feat(cli): add clerk feedback to report CLI problems to Clerk (#566) * Add client-side redaction for feedback text Pure module that scrubs secrets, email addresses and home-directory paths from free text before the upcoming `clerk feedback` command sends it. Nothing calls it yet. - Port the MCP server's redactor (cloudflare-workers `workers/remote-mcp-server/src/redact.ts` at 0d0f978) pattern for pattern, then add the shapes the CLI sees that it misses: `ak_` Platform API keys, `NAME=value` pairs whose name contains SECRET, TOKEN, PASSWORD, API_KEY, PRIVATE_KEY, AUTH or AUTHORIZATION (shell and JSON forms, quoted and unterminated), credentials inside URLs including passwords with `/` or `@`, whole `Authorization:` header values for any scheme, and private-key blocks with or without an END line. - Replace home directories (`/Users/<name>`, `/home/<name>`, `C:\Users\<name>`, `file://` stack-trace paths, JSON-escaped Windows paths, and the live `os.homedir()`) with `~`, leaving API paths such as `/users/user_123` untouched; replace email addresses with `[EMAIL]`. - Return the scrubbed text and an approximate replacement count; a match that is already a marker is not counted twice. - `redactAndCap(text, cap)` cuts to the cap plus 8 KB, scrubs, then cuts to the cap, never leaving a partial marker, so a pathological pattern cannot stall on an unbounded paste and a key block split by the first cut is still caught by the unterminated-block pass. - Every pattern is linear on adversarial inputs (dotted runs, dashed runs, URL-heavy one-line JSON, repeated unmatched BEGIN markers); timing tests pin that. Known limits (username containing a space, URL password over 256 characters, `max_tokens=` over-match, `git@` user segment in URLs, prose naming the Authorization header) are pinned as tests so a later change is deliberate. * Scrub encryption keys and tidy redaction notes - Add ENCRYPTION_KEY to the secret-name words, so `CLERK_ENCRYPTION_KEY=...` and `"dbEncryptionKey": "..."` are redacted; both cases were passing through unchanged. - Rewrite module comments for a public codebase: describe the upstream patterns as a port of Clerk's MCP server redactor, state why `redactAndCap` bounds input before scrubbing, and drop a stale note about the module having no caller. - Use a neutral name in the `author=` prose fixture. * Add clerk feedback command Lets a person or an AI agent report a wrong result, a confusing error or a missing capability to Clerk. Each report is one `CLI_FEEDBACK` event sent over the existing telemetry path, with field names matching the MCP server's feedback tool. - `clerk feedback --goal --attempted --issue --outcome` with optional `--command`, `--error-text`, `--error-code`, `--suggestion`, `--dry-run`, `--yes`; `--input-json` works as on every command. Human mode prompts for missing fields, previews the scrubbed report and asks before sending. Agent mode never prompts: missing or invalid input is a usage error (exit 2), and every outcome after validation prints one JSON object and exits 0. - Every text field goes through `redactAndCap` before anything is built, and the previewed report is the exact object sent. - A telemetry opt-out blocks the report and prints the GitHub issues URL instead; the opt-out is checked before prompting, again after prompting, before the first-run notice, and in the shared sender. - A local cap of 10 reports per machine per rolling 24 hours, plus a refusal of a repeat report for the same command and error code within that window. - `--command` resolves to the same name usage events record (`deploy` to `deploy run`, `open` to `open dashboard`), accepts a pasted command line and rejects typos and groups that run nothing on their own. - Split the telemetry module's event build into a shared envelope, sender and first-run notice so both events reuse them; the per-run usage event and its pinned key list are unchanged. - Disclose the command in README § Telemetry, document it in the command's README, regenerate the help block, and add `minor` changeset. * Harden clerk feedback after review rounds - Report send_failed, exit 0, when the config file cannot be written (no machine id yet, read-only dir) or the first-run notice cannot be recorded, matching how the per-run flush swallows the same failure; the report id is included only when a POST was attempted. - Scrubber: redact `--name value` and `--name=value` flags with a secret-looking name (password, token, api-key, authorization, ...), escaped JSON inside a string, URL credentials with an empty username, and unterminated quoted values containing an escaped quote; never split a surrogate pair at the cap; re-scrub with a doubled bound when earlier redactions pull the pre-cut edge inside the cap, so a token split at the cut can never reach the output. - Dry-run output says whether a real run would send now and why not, with the same reason values as a real run, including send_failed in exactly the states a real run would fail; it never creates a machine id while telemetry is off. - The exported telemetry sender always reads the opt-out itself; the per-run flush calls a module-internal post step after its own read so its one-second job pays no second config read. - Human-mode rate-limit message names the limit and window from the constants and says whether the daily cap or the repeat check applied. - One outcome list drives the help text, the prompt and the shell completion; the test-only command-path export is removed. - READMEs: the cap fails open when the cache file cannot be written; the known loss of a variable name for names the MCP server's patterns also know; wording that scrubbing is pattern-based. * Rework feedback redaction and send handling - Rewrite the redaction module as nine linear passes run on the whole field before the cap, replacing the pre-cut and re-scrub loop, so a split secret cannot occur and no input shape is quadratic - Keep the variable or flag name and redact only the value, including names with spaces (api key: x) and the names the MCP server's patterns know - Catch PGP key blocks, sk- keys, quoted flag values, YAML block scalars and values under a secret-looking YAML parent; keep the closing quote after a Bearer token so the next JSON field still parses; bound key-block header words - End quoted values and separators on their line, so a truncated value cannot swallow the next line - Refuse a text field over 1,000,000 characters as a usage error, since the scrubber reads whole fields - Count a send against the cap when the request may have reached the server, not for connections never made - Print the report id only when a POST was attempted; rethrow Ctrl-C for the interrupt path; let prompt and editor failures surface as their own errors - Show the full posted event (sdk, CLI version, event, payload) in the preview and dry run, which always prints its own shape - Probe config writability with a real temporary-file write instead of rewriting config.json, and share one first-run-notice predicate with the dry run - Accept --command "help <command>", list every attached field in the help text, and record the --outcome completion exception * Tighten feedback scrubbing edge cases - Redact addresses inside brackets (log brackets, Markdown links); the URL-credential marker still cannot be read as an address because a local part cannot end in "]" - Take the rest of the line as the value when a secret-looking name starts a dotenv or YAML line or is followed by a colon, so multi-word passwords are not partly sent; keep one word after a mid-line "=" for logfmt and query strings - Test the secret name once per word and only then the line-start prefix, which keeps the name pass linear and as fast as before - Match flag names with underscores (--api_key, --client_secret) - Run the YAML pass when blanks follow the colon of a parent key, and never take trailing blanks as a value - Stop the literal home-directory pattern from matching a longer user name that starts with it (/Users/shane-old) - Leave hidden subcommands out of the bare-group error for --command - Restore a single config read on the end-of-run notice path, sharing only the CI rule with the dry run's predicate - State exactly what a dry run writes in the help text and both READMEs - Remove an orphaned doc comment and use telemetry's SendOutcome type instead of a shadowing local one * Scrub nested JSON and quoted log values - Redact a whole JSON object or array under a secret-looking key, to its matching close bracket on the line, so inner keys that do not look secret are not sent - End a value that starts inside a quoted string at that string's closing quote, so the rest of a one-line JSON object or log line survives - Treat pretty-printed JSON opened at the end of a key's line, and compact YAML lists at the parent's indentation, as children of a secret-looking parent - Count comment, bullet and numbered-list prefixes as the start of a line for dotenv values - End the literal home-directory match at any character that cannot continue a user name, so punctuation after the home no longer leaks it - Count an interrupted send against the cap before rethrowing when the request may have reached the server - Use named capture groups in the name-value pattern and move a misplaced test comment * Simplify feedback scrubbing to best-effort - Reduce the redaction module to eight passes: private key blocks, Authorization headers, Bearer and Basic tokens, secret-looking names and flags with their value on one line, known key prefixes, URL credentials, home directories and email addresses - Drop the structural passes for YAML blocks, nested and pretty-printed JSON, values inside quoted log strings, clause-length values and Markdown labels, which regressed each time they were patched; their tests go with them, and each dropped shape is pinned as a documented limit - Keep the MCP server's spaced names (api key, client secret, ...) and stop an unquoted value at whitespace or a double quote, so the rest of a one-line log survives - Say in the help text and both READMEs that scrubbing is best-effort, what it catches, what it misses, and that secrets must never be included - Decide whether a failed send counts against the cap only from structured error fields that prove no connection was made, never from message text or the URL, and count an interrupted send that may have landed * Close scrubber gaps and drop ids for unsent reports - Print no report id and do not count against the cap when the error code proves no connection was made, including the codes Bun reports for a refused connection, an unresolvable host and a sandbox block - Redact the token after a Bearer or Basic value that follows a secret-looking name, which the prose Bearer pass skips when the token looks like a word - End an Authorization header that sits inside a quoted string at that string's closing quote, so the rest of a curl command survives and a second header on the line is still scanned - Recognise C:/Users, Git Bash and WSL home paths, and stop a home path's user name before punctuation - Make the README's scrubber description match the code and say best-effort in the changeset * Fix three header and home-path scrub leaks - Count combining marks as part of a user name in both home-path patterns, so a decomposed accent or a Devanagari vowel sign no longer cuts the name short and sends the rest - Stop the whole-line Authorization pattern from matching a header already redacted inside a quoted string, so a later header on the same line is still read - In a header inside a plain-quoted string, end the value only at an unescaped quote (an escaped quote, as in a Digest header, is part of it); in escaped JSON, the escaped quote ends it * Catch more secret shapes and harden the send cap - Accept :=, =>, == and === as assignment separators, so Go, Ruby and PHP assignments and comparisons lose their value - Redact Clerk M2M (mt_) and OAuth access (oat_) tokens of 16 or more letters and digits; @clerk/backend defines only the prefixes, and the length keeps identifiers such as mt_rand out - Treat Cookie and Set-Cookie headers like Authorization headers - Count PASS, PWD and AUTH as secret words when they are a whole segment of a name, camelCase included (dbPass, clientAuth), leaving words such as bypass and passed and the shell's PWD=/path alone - Strip npx, bunx, pnpm dlx and a path to the binary before clerk in --command - Measure the cap's window to the newest recorded send so a backward clock jump does not wipe it, and ignore a record more than one window ahead of now so a clock that ran ahead does not block reports - Say exactly what an agent-mode dry run writes, and that an interrupted send is usually, not always, counted * Scrub camelCase secret flags with a spaced value - The camelCase pass now has the flag form the main name pass has, so `--dbPass hunter2`, `--dbPwd hunter2` and `--clientAuth abc123` lose their values; before, only `--dbPass=hunter2` was caught - The value is one shell word through any quoted part, and a flag followed by another flag is left alone, as for any other flag - Lowercase lookalikes stay (`--bypass`, `--isPassed`, `--clientAuthor`) - Two timing shapes cover the new form, and the command README lists it * Raise the feedback daily cap from 10 to 50 - Shane's decision: a machine 60E1 may now send 50 reports in a rolling 24 hours, up from 10, because 10 is very small - The repeat check is unchanged: a second report with the same --command and --error-code inside the window is still refused - The boundary tests run at the new limit (the 50th report is allowed, the 51st refused), and a new test pins the limit at 50 so the README and the constant cannot drift apart silently - The integration test and the clock-ahead test fill the cap from the constant instead of a hard-coded 10 * Fix feedback review findings - --command accepts npx/bunx/pnpm dlx with runner flags and clerk@<version>, skips known global flags before the command (read from the root's Commander options), and follows Commander's dispatch order so a group's default child wins over its own action - The scrubber catches Clerk dev-browser dvb_ tokens, names containing CREDENTIAL, ACCESS_KEY, SIGNING_KEY or SESSION_KEY, and curl's -u/--user user:password; mysql -p<password> is documented as a limit and two safe over-matches are pinned - --error-code is lowercased, so NOT_LINKED and not_linked are one code for the repeat rule - A dry run shows <assigned when sent> as the report id - A 4xx answer means the report was not stored: no report id, and the send counts toward the daily total but never blocks a repeat; 5xx and timeouts are unchanged - The dry run no longer predicts config-write failures: the writability probe, its notice predicate and their tests are removed, and host-execution.ts matches main again - Test hygiene: restore CI in a telemetry test, drop a stale comment, and name both completion exceptions in the completion rule * Reject version and valued flags in --command - --command treats the version flag as unknown, as help already is, so "--version users list" and "-v users list" are usage errors: that line prints the version and runs no command - A value attached to a global flag that takes none ("--verbose=foo users list") is a usage error, since option parsing fails; "--mode=agent users list" still resolves to "users list" - The curl -u step skips a value only when the whole value is already the marker, so "curl -u [REDACTED]:hunter2" loses the password - The curl patterns now sit above the camelCase pass's comment, which again sits on its own declaration * Check each scrubbed field for its marker - The scrub-before-POST test now checks that each field, as posted, carries the expected replacement ([REDACTED], [EMAIL] or ~/app/index.ts) instead of matching any marker anywhere in the body - This drops the hand-escaped regex that code scanning flagged as incomplete string escaping, and also catches a field whose text is lost entirely * Mention improvements in feedback description - The first line of `clerk feedback --help` now reads "Send Clerk a report about a problem with this CLI, or an improvement that would have helped.", so reports can cover improvements; the required goal, attempted, issue and outcome fields still tie each one to something that was tried - The command README's opening paragraph says the same * Mention improvements in feedback summary - The one-line summary `clerk --help` shows for feedback now reads "Report a wrong result, a confusing error, a missing capability or a needed improvement to Clerk" - The root README's command list and the changeset carry the same wording; the README block is still identical to the rendered help, which its test checks * Close feedback scrubber and argv gaps from review - Scrub again between literal \n/\r/\t, %20/%09/%0A/%0D and ANSI codes, which hid the next secret from every pass - Match long unquoted values in chunks so Bun's regex engine doesn't silently miss them past ~50k chars - Attach required option values as --opt=value so a value like "-v ..." no longer triggers the root's version or help flag (also covers --input-json) - Say CLI_FEEDBACK goes to the telemetry warehouse, not the MCP feedback store Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Close scrubber leaks and quadratic scans from the test run - Scrub again after any URL-encoded character, not only %20/%09/%0A/%0D (a key after `%26key%3D` was sent) - Repeat the quoted-value group once per escape, not per character, so Bun's regex engine no longer skips a quoted value over ~60k characters - Try only the first `eyJ` of a run and the first of a token's trailing dots, so `-eyJ` runs and `Bearer a....a` stay linear Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Fix certificate, cap-file and --command gaps from the test run - A rejected server certificate means nothing was sent: no report id, not counted, the retry goes through - Write the sends file to a temp file and rename it, and read it without Bun.file's stale size, so parallel sends no longer wipe it - Skip root and group flags in --command with their values (`users --mode agent list` → `users list`, `deploy --verbose status` → `deploy status`) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Scrub secrets after JSON \u escapes A key right after a JSON \u escape or a JSON-escaped color code was not at a word edge, so every pass skipped it. Split on both, as on the other escapes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Scrub Authorization headers right after an escape A header right after an escape with a scheme other than Bearer or Basic kept its token (`x\nAuthorization: token abc…`): the name pass took only the scheme word, and the header pass skips a value that starts with the marker. The header pass now accepts an escape before the name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Scrub after colon and written-out ANSI codes; cancel on an empty editor - A key after a colon-separated color code (`ESC[38:5:196m`) or a written-out escape (`\x1B[1m`, `\033[31m`, `\e[1m`) is scrubbed - An empty save in the editor cancels the report: `editor()` reopens at once on a failed check, so an editor that returns before anything is typed relaunched without end Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Keep root flags out of option values; scrub across encoded separators - A root flag typed right after a value option ends the argv rewrite, so the rest parses as Commander reads it: `env pull --file --verbose` is a usage error again, not a file named `--verbose` - `%3D`, `%3A`, `%40` and the same characters JSON-escaped are decoded before scrubbing, so a secret after an encoded `=` or `:`, or an encoded email, is scrubbed Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Take --command as a path; trim feedback docs and slow tests - `--command` is the command path as typed after `clerk`; arguments and flags after the command are dropped. A runner prefix, a flag before the command is complete, or `help` is a usage error instead of a guess - Ctrl-C after the POST prints the result with the report id before the interrupt path exits - `{"my dbPass": …}` is scrubbed: both name=value patterns come from one builder - The feedback suite pins agent mode, so it passes in a terminal; the linear-time tests run at 64 and 128 KB, so redact.test.ts takes about 9 s instead of 66 s - Shorter redact.ts header, feedback.ts comments and `--dry-run` help; the scrubber's known limits live in the feedback README, and the docs no longer say the usage event shares the feedback sender Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Note why the linear-time tests stay under 192 KB Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Devin Gould <devin@clerk.dev> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Drew Sing <drew.sing@clerk.dev>
PreviousNext