diff --git a/CHANGELOG.md b/CHANGELOG.md index 24ead786..6c3aabc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Stop native `--apply` from bloating the patched binary with a large zero-padding gap on Linux/ELF installs: the rebuilt `.bun` section was placed at LIEF's `nextVirtualAddress()` (which rounds up to a coarse boundary) and extending the writable segment to reach it materialized the virtual-address gap as real zero bytes in the file (Claude Code 2.1.218 grew from 273MB to 721MB, ~59% zeros); it is now placed immediately after the writable segment when that segment is topmost, cutting the same patched binary to ~460MB (#915) - @StreamDemon - Stop `--apply` corrupting non-ASCII characters in double- and single-quoted system prompts on native installs: the `\uXXXX` encoding for Bun's Latin-1 module ran before the backslash-doubling from #664, so the doubling escaped the backslash the encoder had just emitted and an em dash reached Claude Code as the literal text `\u2014`; the encoding now runs after the string-literal escaping (#920) - @StreamDemon - Decode `\"` and `\'` when generating a system prompt's markdown, so the file shows the prompt text instead of JavaScript string-literal syntax and `--apply` stops embedding a real backslash where the bundle only had a quote; this is applied to prompts that are plain text end to end, since inside a `${...}` interpolation those escapes belong to nested JavaScript and decoding them would make the prompt look unterminated and be skipped (#921) - @StreamDemon +- Add an opt-in patch that prevents Claude Code from loading enterprise settings delivered by Anthropic or a gateway (#725) - @mike1858 ## [v4.3.2](https://github.com/Piebald-AI/tweakcc/releases/tag/v4.3.2) - 2026-07-20 diff --git a/src/applyPlan.ts b/src/applyPlan.ts index 3987d1f9..b842a17b 100644 --- a/src/applyPlan.ts +++ b/src/applyPlan.ts @@ -145,6 +145,8 @@ export function isPatchEnabledByConfig( return !!misc?.suppressRateLimitOptions; case 'suppress-rate-limit-warning': return !!misc?.suppressRateLimitWarning; + case 'disable-server-managed-settings': + return !!misc?.disableServerManagedSettings; case 'token-count-rounding': return !!misc?.tokenCountRounding; case 'remember-skill': diff --git a/src/defaultSettings.ts b/src/defaultSettings.ts index 664ede33..d8919726 100644 --- a/src/defaultSettings.ts +++ b/src/defaultSettings.ts @@ -701,6 +701,7 @@ export const DEFAULT_SETTINGS: Settings = { suppressLineNumbers: false, suppressRateLimitOptions: false, suppressRateLimitWarning: false, + disableServerManagedSettings: false, mcpConnectionNonBlocking: true, mcpServerBatchSize: null, statuslineThrottleMs: null, diff --git a/src/patches/disableServerManagedSettings.test.ts b/src/patches/disableServerManagedSettings.test.ts new file mode 100644 index 00000000..bb7a84a4 --- /dev/null +++ b/src/patches/disableServerManagedSettings.test.ts @@ -0,0 +1,96 @@ +import { describe, expect, it } from 'vitest'; +import { writeDisableServerManagedSettings } from './disableServerManagedSettings'; + +const makeEligibilityGate = ( + delimiter = '}', + names = { + fn: 'cEe', + cache: 'lEe', + overridePath: 'JOe', + recordDecision: 'XOe', + provider: 'Hn', + gatewayConfig: 'Cy', + isGatewayEnabled: 'B5e', + } +) => + `${delimiter}function ${names.fn}(){if(${names.cache}!==void 0)return ${names.cache};if(${names.overridePath}())return ${names.cache}=${names.recordDecision}(!0);if(${names.provider}()==="gateway")return ${names.cache}=${names.recordDecision}(${names.isGatewayEnabled}(${names.gatewayConfig}()));if(${names.provider}()!=="firstParty")return ${names.cache}=${names.recordDecision}(!1);return ${names.cache}=${names.recordDecision}(!1)}`; + +describe('writeDisableServerManagedSettings', () => { + it('disables the Claude Code 2.1.220 remote settings eligibility gate', () => { + const input = + makeEligibilityGate() + + 'function JUs(){if(cEe())Pmt=new Promise(e=>{})}' + + 'function Vu_(){return `${Rs().BASE_API_URL}/api/claude_code/settings`}'; + + const result = writeDisableServerManagedSettings(input); + + expect(result).not.toBeNull(); + expect(result).toContain('function cEe(){return !1;if(lEe!==void 0)'); + expect(result).toContain('function JUs(){if(cEe())'); + expect(result).toContain('/api/claude_code/settings'); + }); + + it('supports the formatted Claude Code 2.1.220 syntax', () => { + const input = `} + function cEe() { + if (lEe !== void 0) return lEe; + if (JOe()) return (lEe = XOe(!0)); + if (Hn() === "gateway") return (lEe = XOe(B5e(Cy()))); + if (Hn() !== "firstParty") return (lEe = XOe(!1)); + return (lEe = XOe(!1)); + } +`; + + const result = writeDisableServerManagedSettings(input); + + expect(result).not.toBeNull(); + expect(result).toMatch(/function cEe\(\) \{\s*return !1;/); + }); + + it('matches independently of minified identifier names', () => { + const input = makeEligibilityGate(';', { + fn: '$a', + cache: 'B$2', + overridePath: 'Q0', + recordDecision: 'r$9', + provider: 'zZ', + gatewayConfig: 'C$y', + isGatewayEnabled: 'm4', + }); + + const result = writeDisableServerManagedSettings(input); + + expect(result).not.toBeNull(); + expect(result).toContain('function $a(){return !1;if(B$2!==void 0)'); + }); + + it('returns the file unchanged when already patched', () => { + const patched = writeDisableServerManagedSettings(makeEligibilityGate())!; + + expect(writeDisableServerManagedSettings(patched)).toBe(patched); + }); + + it('supports all efficient boundary delimiters', () => { + for (const delimiter of [',', ';', '}', '{']) { + const result = writeDisableServerManagedSettings( + makeEligibilityGate(delimiter) + ); + expect(result).not.toBeNull(); + expect(result).toContain('(){return !1;if('); + } + }); + + it('does not patch a generic memoized boolean helper', () => { + const input = + '}function other(){if(cache!==void 0)return cache;if(hasOverride())return cache=record(!0);return cache=record(!1)}'; + + expect(writeDisableServerManagedSettings(input)).toBeNull(); + }); + + it('returns null for a partial remote settings gate', () => { + const input = + '}function cEe(){if(lEe!==void 0)return lEe;if(JOe())return lEe=XOe(!0);if(Hn()==="gateway")return true}'; + + expect(writeDisableServerManagedSettings(input)).toBeNull(); + }); +}); diff --git a/src/patches/disableServerManagedSettings.ts b/src/patches/disableServerManagedSettings.ts new file mode 100644 index 00000000..3ac850b6 --- /dev/null +++ b/src/patches/disableServerManagedSettings.ts @@ -0,0 +1,54 @@ +import { debug } from '../utils'; +import { showDiff } from './index'; + +/** + * Disable Claude Code's server-managed settings eligibility gate. + * + * This gate controls the initial load, cache use, refresh, and polling paths for + * settings delivered by Anthropic's API or a configured gateway. Local managed + * settings files use separate loaders and intentionally remain unaffected. + * + * CC 2.1.220: + * ```diff + * function cEe(){ + * + return !1; + * if(lEe!==void 0)return lEe; + * if(JOe())return lEe=XOe(!0); + * if(Hn()==="gateway")return lEe=XOe(B5e(Cy())); + * ``` + */ +export const writeDisableServerManagedSettings = ( + oldFile: string +): string | null => { + const alreadyPatched = + /[,;{}]\s*function\s+([$\w]+)\s*\(\s*\)\s*\{\s*return\s+!1;\s*if\s*\(\s*([$\w]+)\s*!==\s*void\s+0\s*\)\s*return\s+\2;\s*if\s*\(\s*([$\w]+)\s*\(\s*\)\s*\)\s*return\s+\(?\s*\2\s*=\s*[$\w]+\s*\(\s*!0\s*\)\s*\)?;\s*if\s*\(\s*([$\w]+)\s*\(\s*\)\s*===\s*"gateway"\s*\)\s*return\s+\(?\s*\2\s*=\s*[$\w]+\s*\(/; + if (alreadyPatched.test(oldFile)) return oldFile; + + // The override-file and gateway branches uniquely identify the remote-settings + // eligibility helper without depending on minified names from a specific build. + const pattern = + /[,;{}]\s*function\s+([$\w]+)\s*\(\s*\)\s*\{\s*if\s*\(\s*([$\w]+)\s*!==\s*void\s+0\s*\)\s*return\s+\2;\s*if\s*\(\s*([$\w]+)\s*\(\s*\)\s*\)\s*return\s+\(?\s*\2\s*=\s*[$\w]+\s*\(\s*!0\s*\)\s*\)?;\s*if\s*\(\s*([$\w]+)\s*\(\s*\)\s*===\s*"gateway"\s*\)\s*return\s+\(?\s*\2\s*=\s*[$\w]+\s*\(/; + const match = oldFile.match(pattern); + + if (!match || match.index === undefined) { + debug( + 'patch: disableServerManagedSettings: failed to find remote settings eligibility gate' + ); + return null; + } + + const functionBodyOffset = match[0].indexOf('{', 1) + 1; + if (functionBodyOffset === 0) { + debug( + 'patch: disableServerManagedSettings: matched eligibility gate without a function body' + ); + return null; + } + const insertIndex = match.index + functionBodyOffset; + const insertion = 'return !1;'; + const newFile = + oldFile.slice(0, insertIndex) + insertion + oldFile.slice(insertIndex); + + showDiff(oldFile, newFile, insertion, insertIndex, insertIndex); + return newFile; +}; diff --git a/src/patches/index.ts b/src/patches/index.ts index eb1442f8..0ba0dedc 100644 --- a/src/patches/index.ts +++ b/src/patches/index.ts @@ -64,6 +64,7 @@ import { writeIncreaseFileReadLimit } from './increaseFileReadLimit'; import { writeSuppressLineNumbers } from './suppressLineNumbers'; import { writeSuppressRateLimitOptions } from './suppressRateLimitOptions'; import { writeSuppressRateLimitWarning } from './suppressRateLimitWarning'; +import { writeDisableServerManagedSettings } from './disableServerManagedSettings'; import { writeSessionMemory } from './sessionMemory'; import { writeRememberSkill } from './rememberSkill'; import { writeThinkingBlockStyling } from './thinkingBlockStyling'; @@ -355,6 +356,13 @@ const PATCH_DEFINITIONS = [ description: 'Rate limit warning banners will be suppressed (errors still shown)', }, + { + id: 'disable-server-managed-settings', + name: 'Disable server-managed settings', + group: PatchGroup.MISC_CONFIGURABLE, + description: + 'Prevent Claude Code from loading enterprise settings delivered by a server', + }, { id: 'token-count-rounding', name: 'Token count rounding', @@ -859,6 +867,10 @@ export const applyCustomization = async ( fn: c => writeSuppressRateLimitWarning(c), condition: !!config.settings.misc?.suppressRateLimitWarning, }, + 'disable-server-managed-settings': { + fn: c => writeDisableServerManagedSettings(c), + condition: !!config.settings.misc?.disableServerManagedSettings, + }, 'token-count-rounding': { fn: c => writeTokenCountRounding(c, config.settings.misc!.tokenCountRounding!), diff --git a/src/tests/applyPlan.test.ts b/src/tests/applyPlan.test.ts index 1a3edd78..d589a9a9 100644 --- a/src/tests/applyPlan.test.ts +++ b/src/tests/applyPlan.test.ts @@ -58,6 +58,41 @@ describe('applyPlan', () => { ).toBe(false); }); + it('includes server-managed settings patch only when enabled', () => { + const disabled = configWithDefaults(); + const enabled = configWithDefaults({ + misc: { + ...DEFAULT_SETTINGS.misc, + disableServerManagedSettings: true, + }, + }); + + expect( + isPatchEnabledByConfig( + 'disable-server-managed-settings', + disabled, + '2.1.220' + ) + ).toBe(false); + expect( + isPatchEnabledByConfig( + 'disable-server-managed-settings', + enabled, + '2.1.220' + ) + ).toBe(true); + expect( + getPlannedPatches(disabled, '2.1.220', [ + 'disable-server-managed-settings', + ]) + ).toEqual([]); + expect( + getPlannedPatches(enabled, '2.1.220', [ + 'disable-server-managed-settings', + ]).map(p => p.id) + ).toEqual(['disable-server-managed-settings']); + }); + it('respects --patches filter', () => { const planned = getPlannedPatches(configWithDefaults(), '2.1.200', [ 'session-memory', diff --git a/src/types.ts b/src/types.ts index ec8ee8a2..6984081c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -119,6 +119,7 @@ export interface MiscConfig { suppressLineNumbers: boolean; suppressRateLimitOptions: boolean; suppressRateLimitWarning: boolean; + disableServerManagedSettings: boolean; mcpConnectionNonBlocking: boolean; mcpServerBatchSize: number | null; statuslineThrottleMs: number | null; diff --git a/src/ui/components/MiscView.tsx b/src/ui/components/MiscView.tsx index 6569f7b1..b1e9d370 100644 --- a/src/ui/components/MiscView.tsx +++ b/src/ui/components/MiscView.tsx @@ -67,6 +67,7 @@ export function MiscView({ onSubmit }: MiscViewProps) { suppressLineNumbers: false, suppressRateLimitOptions: false, suppressRateLimitWarning: false, + disableServerManagedSettings: false, mcpConnectionNonBlocking: true, mcpServerBatchSize: null as number | null, statuslineThrottleMs: null as number | null, @@ -340,6 +341,20 @@ export function MiscView({ onSubmit }: MiscViewProps) { }); }, }, + { + id: 'disableServerManagedSettings', + title: 'Disable server-managed settings', + description: + 'Prevents Claude Code from downloading enterprise settings from Anthropic or a gateway. Local managed settings still apply.', + getValue: () => settings.misc?.disableServerManagedSettings ?? false, + toggle: () => { + updateSettings(settings => { + ensureMisc(); + settings.misc!.disableServerManagedSettings = + !settings.misc!.disableServerManagedSettings; + }); + }, + }, { id: 'mcpNonBlocking', title: 'Non-blocking MCP startup',