Skip to content

'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

Closed

Description

@proggeramlug

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.

Likely relationship to #9674

#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

  1. 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(...)).
  2. 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.
  3. 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.

Activity

  1. proggeramlug commented on Sep 4, 2026

    @proggeramlug
    ContributorAuthor

    Correction: this issue's key observable is wrong, and the bug did not reproduce under exhaustive testing.

    1. 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.

    2. 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.

    3. 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.

  2. added a commit that references this issue on Sep 4, 2026
    d88e8b5
  3. added a commit that references this issue on Sep 4, 2026
    787e302
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions