You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
'Yes, and save this' permission fails with '(number).slice is not a function' — a value that must be a string is a number; the rule is never persisted #9675
Deterministic, reproduced on demand, with a checkable observable.cc_manual @ f6aac9e5c4, macOS, sandboxed HOME.
Steps: ask cc to create a file → permission dialog appears → choose "Yes, and save this" (the persist-the-rule option, not plain Yes).
ERROR (number).slice is not a function
at rB8.getLoadBalancerName (<anonymous>)
at jxY (<anonymous>)
at s (<anonymous>)
at Mu_ (<anonymous>)
at WN8.calculateLayout (<anonymous>)
at get destination (<anonymous>)
Observable confirming the effect: .claude.json is rewritten at that exact second (mtime matches, numStartups advanced) but the project's allowedTools remains [] — the permission rule is never persisted. Plain "Yes" works and the write succeeds; only the save-the-rule variant fails.
What the error says
.slice exists on strings and arrays, never on numbers, and the receiver is reported as (number). So a value that node holds as a string is a number under perry at the point where the permission rule is built (a rule string like Write(path) is the obvious candidate for slicing — tool name, path prefix, or rule parsing).
This is the same family as the tag-ambiguity bugs fixed earlier in this campaign (#9415: class refs printing as integers because 0x7FFE is shared between INT32 and class-ref; #9462's hole-leaks answering wrong-kind values). A string-typed value arriving as a number is the wrong-kind-decode signature, now on a live user path.
#9674 recorded config keys (hasCompletedOnboarding, lastOnboardingVersion) vanishing from .claude.json with an unidentified trigger. This is a strong candidate for that trigger: an exception thrown during the config read-modify-write would abort it partway, and whatever the writer had already staged determines what survives. In this capture the onboarding keys happened to survive while allowedTools was lost — consistent with an aborted update rather than a clean write. Worth testing #9674 by hitting this path repeatedly.
Repro / verification bar
Fresh project directory, ask for a file write, choose "Yes, and save this" → error, allowedTools stays []. Compare against node: the rule should appear in allowedTools (e.g. Write(...)).
Reduce: instrument or bisect the rule-construction path to find which value is a number; then a fixture pinning that value's type. The minified frames here are unreliable (getLoadBalancerName, WN8.calculateLayout are feat(runtime): real function names in Error stacks — frame-pointer walk + the existing name registry (#9486) #9521 name-borrowing artifacts — WN8 is the bundle's JS yoga replacement and cannot be on this path), so instrument rather than trust the trace.
Regression arms: plain "Yes" (must keep working), "Yes, allow all edits this session", and a second run confirming the persisted rule suppresses the dialog.
Severity: high — "don't ask me again" is unusable, and every use of it corrupts a config write.
Correction: this issue's key observable is wrong, and the bug did not reproduce under exhaustive testing.
projects[<cwd>].allowedTools in .claude.json is LEGACY in cc 2.1.112 — nothing writes it.Ew() (bundle @3746869) only reads it; there is no write site in the bundle. Rules are persisted to .claude/settings.local.json, and under perry they land there correctly. So "the rule is never persisted" was a false negative from checking the wrong file. The only hard evidence in this issue is the error text itself.
Did not reproduce, across every rule-persisting dialog in the bundle with the persist option selected, on a clean sandbox HOME and a clone of the real one, in the reporting cwd: Write in-project / out-of-project / into .claude/ (exercising the VL8.slice(0,-2) branch) / 300-line content; Read/Glob outside workspace; WebFetch; WebSearch; Bash touch/mkdir/rm/chmod/pip/npm/curl; a 4-dialog session; /permissions → add-rule → destination picker; and a settings.local.json already holding backslash-escaped rules. Zero occurrences; every rule persisted.
Also cleared: 45 receiver shapes for a dynamic .slice (SSO lengths 0/1/5/6/40, Map/Set spreads, getters, class fields, closures, flatMap/Object.entries/JSON.parse, concat/template/replace/trim/split/join, destructuring defaults, optional chaining, plus cc's exact ruleContent.slice(0,-2) / key.slice(-20) / name.slice(0,-6)) — all byte-identical to node under the exact compiler that built the reporting binary. Forced-evacuating GC and 2-byte-chunked SSE changed nothing.
What is now pinned exactly: the (number) label is emitted only by native_call_method.rs:2414-2455 — the dynamic dispatch tower. So codegen did not prove the receiver a string (a proven string routes through lower_string_method.rs, which hard-codes kind="string"), is_int32() || is_number() was true, is_any_string() false, and the bare-heap-pointer recovery just above either didn't apply or its validation rejected the pointer. So the receiver was a genuine number or a stale/unrecoverable bare pointer — not an SSO short-string misread (disproved empirically).
The one step that closes this: --debug-symbols puts the source location in the message, verified end-to-end on this compiler — MSG:(number).slice is not a function with at <file>:<line> instead of <anonymous>. Rebuilding cc once with that flag makes the next occurrence self-locating.
Also worth checking when that line arrives: saveConfigWithLock (bundle @3743200) does V.slice(v) over a readdirSync result inside a catch that only logs — a plausible home for a swallowed(number).slice that would not block the write.
A reusable offline harness (mock SSE API + PTY driver, no network, no token) is at secret-tests/cc-permission-harness/ and reproduces every cc permission dialog deterministically.
Activity
proggeramlug commentedon Sep 4, 2026
Correction: this issue's key observable is wrong, and the bug did not reproduce under exhaustive testing.
projects[<cwd>].allowedToolsin.claude.jsonis LEGACY in cc 2.1.112 — nothing writes it.Ew()(bundle @3746869) only reads it; there is no write site in the bundle. Rules are persisted to.claude/settings.local.json, and under perry they land there correctly. So "the rule is never persisted" was a false negative from checking the wrong file. The only hard evidence in this issue is the error text itself.Did not reproduce, across every rule-persisting dialog in the bundle with the persist option selected, on a clean sandbox HOME and a clone of the real one, in the reporting cwd:
Writein-project / out-of-project / into.claude/(exercising theVL8.slice(0,-2)branch) / 300-line content;Read/Globoutside workspace;WebFetch;WebSearch;Bashtouch/mkdir/rm/chmod/pip/npm/curl; a 4-dialog session;/permissions→ add-rule → destination picker; and asettings.local.jsonalready holding backslash-escaped rules. Zero occurrences; every rule persisted.Also cleared: 45 receiver shapes for a dynamic
.slice(SSO lengths 0/1/5/6/40, Map/Set spreads, getters, class fields, closures,flatMap/Object.entries/JSON.parse, concat/template/replace/trim/split/join, destructuring defaults, optional chaining, plus cc's exactruleContent.slice(0,-2)/key.slice(-20)/name.slice(0,-6)) — all byte-identical to node under the exact compiler that built the reporting binary. Forced-evacuating GC and 2-byte-chunked SSE changed nothing.What is now pinned exactly: the
(number)label is emitted only bynative_call_method.rs:2414-2455— the dynamic dispatch tower. So codegen did not prove the receiver a string (a proven string routes throughlower_string_method.rs, which hard-codeskind="string"),is_int32() || is_number()was true,is_any_string()false, and the bare-heap-pointer recovery just above either didn't apply or its validation rejected the pointer. So the receiver was a genuine number or a stale/unrecoverable bare pointer — not an SSO short-string misread (disproved empirically).The one step that closes this:
--debug-symbolsputs the source location in the message, verified end-to-end on this compiler —MSG:(number).slice is not a functionwithat <file>:<line>instead of<anonymous>. Rebuilding cc once with that flag makes the next occurrence self-locating.Also worth checking when that line arrives:
saveConfigWithLock(bundle @3743200) doesV.slice(v)over areaddirSyncresult inside acatchthat only logs — a plausible home for a swallowed(number).slicethat would not block the write.A reusable offline harness (mock SSE API + PTY driver, no network, no token) is at
secret-tests/cc-permission-harness/and reproduces every cc permission dialog deterministically.fix(runtime): decide bare managed receivers at the dispatch tower's e…
fix(runtime): decide bare managed receivers at the dispatch tower's e…
fix(runtime): a boxed-primitive probe reads a bare word only when the…