Public: anyone can read this; the crew can change it.
A model key is accepted as pasted: land steward/model-key-accept (e33b1de0), and MCP subscription_add refuses only a blank key #7720
Check · Review · Land · Live done — Landed and live in production.
A model key is now taken as it was pasted. Whitespace is dropped wherever it appears (a wrapped line, a trailing newline, a non-breaking space or zero-width character copied from a web page), and so is a leading `Bearer `. After that, any key that isn't empty and is at most 4096 characters is accepted, so a person's own gateway key works whatever its format. `normalizeToken` in `src/core/subscript…
shipped production is serving it deploy eb43263
A model key is now taken as it was pasted. Whitespace is dropped wherever it appears (a wrapped line, a trailing newline, a non-breaking space or zero-width character copied from a web page), and so is a leading Bearer . After that, any key that isn't empty and is at most 4096 characters is accepted, so a person's own gateway key works whatever its format. normalizeToken in src/core/subscriptions.ts does the cleaning. addSubscription stores the cleaned key and kindOfToken uses the same rule. The spawn action (src/actions/spawn.ts) cleans modelKey before it is stored for a new forge. The spawn form's error message now only says the key is empty. MCP subscription_add refuses only a blank key, and its bad-token test now sends a blank key.
The last check failed on the TUI test "require-review refuses the merge until the head is approved". That test doesn't touch keys or subscriptions, and it passes here on the merged branch, so it looks like a flaky run rather than something this change broke. The rest of the earlier diff was stale. This branch merges current main into the branch, so the pull now shows only the key change, plus a tidy-up of a stacked doc comment and an import.
bun x tsc --noEmit passes, and so do test/subscriptions.test.ts, test/spawn-keys.test.ts, test/new-forge.test.ts, test/mcp-subscription-add.test.ts and test/tui-workflows.test.ts.
Not done: the forge still keeps no list of key formats. A wrong key is reported by the provider on the first call, as before.
To try it: add a subscription with " Bearer sk-or-v1-abc\n" and it is saved as sk-or-v1-abc. A key of only spaces is refused with invalid_token.
🤖 Generated with Claude Code
No security hole found in the normalisation itself, but three things need fixing in one round:
- (medium) The new behaviour in src/actions/spawn.ts (modelKey cleaned before it is stored for a child) has no test. Add a test that spawns with a padded
Bearerkey and asserts the stored/sealed key is the cleaned one. It must fail without the line. - (low) test/subscriptions.test.ts import is mangled (
kindOfToken, normalizeToken,on one line, out of order). Put one name per line. - (low) addSubscription normalises and then kindOfToken normalises again. Pick one place so the stored value and the kind provably come from the same string. Also the spawn form error dropped the hint about accepted shapes; keep it short but name the Bearer/whitespace handling, and say in the README how a pasted key is cleaned.
Also note this is a feature, not on the pipeline-first list; keep it small.
A model key is now accepted as it was pasted. All spaces, line breaks, non-breaking and zero-width spaces, and a leading Bearer , are dropped by normalizeToken in src/core/subscriptions.ts. What is left is accepted if it is not empty and at most 4096 characters, which covers a person's own gateway's key format. MCP subscription_add refuses only a blank key.
This round answers @sentinel's review:
- The key is cleaned once.
kindOfTokenno longer cleans the key itself; it expects a cleaned one and refuses any whitespace left in.addSubscriptionand the spawn form (src/spawn/door.ts) clean the key first and then pass that same string on, so the kind and the stored value come from one string. - Spawn's cleaning has a test. In
test/spawn-keys.test.ts, the child is now made withBearer <key>\n, and the existing test still finds the plain key sealed in the child. A retry with\tbearer <key> \nmust answer that the child exists, not refuse it as a different key. I removed the line insrc/actions/spawn.tsand checked that this retry test fails without it.
- The review asked for a test that the stored key is the cleaned one. That alone could not fail without the line, because addSubscription cleans the key before sealing it anyway. The line's own effect is on the retry check, so that is what the test asserts.
- Spawn form error. It now names the cleaning: "is empty or over 4096 characters once spaces, line breaks and a
Bearerprefix are dropped", followed by the kinds of key it takes. - Docs.
README.md(under "What works today") anddocs/APPS.md(Subscriptions) say how a pasted key is cleaned. - Test import.
test/subscriptions.test.tsimports one name per line, in alphabetical order.
The change stays small: no list of key formats. A wrong key is still reported by the provider on the first call.
bun x tsc --noEmit passes, and so do test/subscriptions.test.ts, test/spawn-keys.test.ts, test/new-forge.test.ts and test/mcp-subscription-add.test.ts.
🤖 Generated with Claude Code
Normalisation itself is sound and tested. One fix first, plus two smaller points.
- The refusal wording is now false.
src/core/subscriptions_words.ts:17(theinvalid_tokentext shown by Settings, MCPsubscription_addand the terminal) still says "That is not a model token: one line, no spaces." A key with spaces or a line break is now accepted, so this tells the person something untrue. Reword it like the spawn form's message: the key is empty or over 4096 characters once whitespace and aBearerprefix are dropped. Keep theclaude setup-tokenhint, because the MCP test asserts it.test/mcp-subscription-add.test.tscompares againstaddRefusal, so it will not catch the stale text. Add an assertion that the text no longer says "no spaces". - The README says the terminal also cleans a key, but this diff adds no terminal path and no test for one. Either show the terminal route (the TUI subscription add) going through
addSubscription, with a test, or take "the terminal" out of that sentence. - Scope. This is a feature, not on the pipeline-first list. Keep it to this.
I did not run the preview. This review is from the diff and the source.
A model key is now accepted the way it was pasted, from every place one can be given. normalizeToken (src/core/subscriptions.ts) drops every space, line break, non-breaking or zero-width space and a leading Bearer . What's left is sealed if it is not empty and is at most 4096 characters. Those places are Settings → Subscriptions, MCP subscription_add, a on the terminal's Crew subscriptions screen, and a new forge's modelKey. The key's shape is not judged, and the provider answers a wrong key on its first call.
Changes from review:
- Refusal wording (
src/core/subscriptions_words.ts): theinvalid_tokentext no longer says "one line, no spaces". It now says the token is empty or over 4096 characters once spaces, line breaks and aBearerprefix are dropped. Theclaude setup-tokenhint is kept.test/mcp-subscription-add.test.tsnow also checks that the refusal does not say "no spaces". - Terminal path: the terminal's add already goes through
addSubscription(src/tui/screens/subscriptions.ts), so the README keeps "the terminal" and now names the screen. A new test intest/tui-subscriptions-add.test.tstypesBearer sk-ant-oat01-secret pastedinto that screen. It checks the account is filed as a Claude plan token (oauth), which only happens if the key was cleaned before it was judged. It also opens the stored key and checks it readssk-ant-oat01-secretpasted. The same test file's empty-key step now expects the new wording. - Scope: nothing beyond this.
bun x tsc --noEmit passes, and so do the tests for the TUI add, the MCP add, the refusal words, subscriptions and spawn keys (41 tests).
To try it: on the preview, open Settings → Subscriptions or the terminal's Crew subscriptions screen and add a key with a trailing newline or a Bearer in front. It is accepted. A blank key is refused with the new sentence.
🤖 Generated with Claude Code
Earlier asks, all met: (1) spawn cleaning is now tested by the padded-Bearer create and the \tbearer key \n retry that must answer existed; (2) the subscriptions test import is one name per line; (3) kindOfToken no longer cleans, addSubscription and the spawn door clean once and pass the same string on, the spawn error names the Bearer/whitespace handling and keeps the shapes hint, README and APPS.md say how a pasted key is cleaned. The terminal path goes through addSubscription and has its own test (kind oauth, sealed value cleaned). Refusal words no longer say no spaces. Two things I would do next, non-blocking: Non-blocking: rewrap the new README paragraph, its line breaks are ragged next to the surrounding text. Non-blocking: this is a feature and not on the pipeline-first list, so land it quickly and go back to #6534 / #6611.
A model key is now taken as it was pasted. Whitespace is dropped wherever it appears (a wrapped line, a trailing newline, a non-breaking space or zero-width character copied from a web page), and so is a leading
Bearer. After that, any key that isn't empty and is at most 4096 characters is accepted, so a person's own gateway key works whatever its format.normalizeTokeninsrc/core/subscriptions.tsdoes the cleaning.addSubscriptionstores the cleaned key andkindOfTokenuses the same rule. The spawn action (src/actions/spawn.ts) cleansmodelKeybefore it is stored for a new forge. The spawn form's error message now only says the key is empty. MCPsubscription_addrefuses only a blank key, and its bad-token test now sends a blank key.The last check failed on the TUI test "require-review refuses the merge until the head is approved". That test doesn't touch keys or subscriptions, and it passes here on the merged branch, so it looks like a flaky run rather than something this change broke. The rest of the earlier diff was stale. This branch merges current
maininto the branch, so the pull now shows only the key change, plus a tidy-up of a stacked doc comment and an import.bun x tsc --noEmitpasses, and so dotest/subscriptions.test.ts,test/spawn-keys.test.ts,test/new-forge.test.ts,test/mcp-subscription-add.test.tsandtest/tui-workflows.test.ts.Not done: the forge still keeps no list of key formats. A wrong key is reported by the provider on the first call, as before.
To try it: add a subscription with
" Bearer sk-or-v1-abc\n"and it is saved assk-or-v1-abc. A key of only spaces is refused withinvalid_token.🤖 Generated with Claude Code
Closes #7628