From 279fd9aae689ce82bec26d19d5e6a0a6e0912c59 Mon Sep 17 00:00:00 2001 From: Mike Date: Sat, 24 Jan 2026 11:47:11 -0700 Subject: [PATCH 1/4] Add patch to prevent updates to unsupported versions --- src/defaultSettings.ts | 1 + src/patches/index.ts | 6 + src/patches/preventUnsupportedUpdates.ts | 139 +++++++++++++++++++++++ src/types.ts | 1 + src/ui/components/MiscView.tsx | 16 +++ 5 files changed, 163 insertions(+) create mode 100644 src/patches/preventUnsupportedUpdates.ts diff --git a/src/defaultSettings.ts b/src/defaultSettings.ts index f25f2c43..d4d3d1bd 100644 --- a/src/defaultSettings.ts +++ b/src/defaultSettings.ts @@ -755,6 +755,7 @@ export const DEFAULT_SETTINGS: Settings = { increaseFileReadLimit: false, suppressLineNumbers: true, suppressRateLimitOptions: false, + preventUpdateToUnsupportedVersions: false, }, toolsets: [], defaultToolset: null, diff --git a/src/patches/index.ts b/src/patches/index.ts index 424bfd28..63ef8558 100644 --- a/src/patches/index.ts +++ b/src/patches/index.ts @@ -70,6 +70,7 @@ import { restoreClijsFromBackup, } from '../installationBackup'; import { compareVersions } from '../systemPromptSync'; +import { writePreventUnsupportedUpdates } from './preventUnsupportedUpdates'; export interface LocationResult { startIndex: number; @@ -736,6 +737,11 @@ export const applyCustomization = async ( if ((result = writeSuppressRateLimitOptions(content))) content = result; } + // Apply prevent update to unsupported versions patch (if enabled) + if (config.settings.misc?.preventUpdateToUnsupportedVersions) { + if ((result = writePreventUnsupportedUpdates(content))) content = result; + } + // Write the modified content back if (ccInstInfo.nativeInstallationPath) { // For native installations: repack the modified claude.js back into the binary diff --git a/src/patches/preventUnsupportedUpdates.ts b/src/patches/preventUnsupportedUpdates.ts new file mode 100644 index 00000000..35863c76 --- /dev/null +++ b/src/patches/preventUnsupportedUpdates.ts @@ -0,0 +1,139 @@ +// Please see the note about writing patches in ./index +// +// This patch prevents Claude Code from auto-updating to versions that tweakcc +// doesn't yet support. It works by checking if the prompts file exists on GitHub +// for the target version before allowing the update. + +import { LocationResult, showDiff } from './index'; + +/** + * Finds the location in the auto-updater where the latest version is fetched + * and the update decision is made. + * + * The pattern we're looking for (minified): + * BUILD_TIME:"..."}.VERSION,CHANNEL_VAR=hq()?.autoUpdatesChannel??"latest",VERSION_VAR=await FUNC(CHANNEL_VAR),OTHER_VAR=FUNC2(); + * + * We'll inject code after VERSION_VAR assignment to check if it's supported. + */ +const getAutoUpdaterLocation = (oldFile: string): LocationResult | null => { + // Pattern to match the auto-updater version fetch in minified code + // The key markers are: + // - BUILD_TIME:"..." followed by }.VERSION, + // - hq()?.autoUpdatesChannel??"latest" + // - await FUNC(VAR) pattern + // Captures: [1]=channel var, [2]=version var, [3]=fetch function, [4]=next var, [5]=next func + const pattern = + /BUILD_TIME:"[^"]+"\}\.VERSION,([$\w]+)=hq\(\)\?\.autoUpdatesChannel\?\?"latest",([$\w]+)=await ([$\w]+)\(\1\),([$\w]+)=([$\w]+)\(\);/; + + const match = oldFile.match(pattern); + + if (!match || match.index === undefined) { + console.error( + 'patch: preventUnsupportedUpdates: failed to find auto-updater pattern' + ); + return null; + } + + return { + startIndex: match.index, + endIndex: match.index + match[0].length, + identifiers: [ + match[0], // Full match + match[1], // channel var (e.g., _) + match[2], // version var (e.g., Z) + match[3], // fetch function (e.g., _t) + match[4], // next var (e.g., G) + match[5], // next var's function (e.g., Ed) + ], + }; +}; + +/** + * Gets the variable name used for the current version. + * This is typically $ in the code pattern: + * let $={...}.VERSION, + */ +const getCurrentVersionVar = ( + oldFile: string, + autoUpdaterLocation: LocationResult +): string | null => { + // Look backwards from our match to find the current version variable + // Pattern: let CURRENT_VAR={...ISSUES_EXPLAINER:... + const searchStart = Math.max(0, autoUpdaterLocation.startIndex - 500); + const searchChunk = oldFile.slice( + searchStart, + autoUpdaterLocation.startIndex + ); + + // Find the last "let VAR={" pattern with ISSUES_EXPLAINER reference (minified format) + const pattern = /let ([$\w]+)=\{[^}]*ISSUES_EXPLAINER:/g; + let lastMatch = null; + let match; + + while ((match = pattern.exec(searchChunk)) !== null) { + lastMatch = match; + } + + if (!lastMatch) { + console.error( + 'patch: preventUnsupportedUpdates: failed to find current version variable' + ); + return null; + } + + return lastMatch[1]; +}; + +export const writePreventUnsupportedUpdates = ( + oldFile: string +): string | null => { + const location = getAutoUpdaterLocation(oldFile); + if (!location) { + return null; + } + + const currentVersionVar = getCurrentVersionVar(oldFile, location); + if (!currentVersionVar) { + return null; + } + + const channelVar = location.identifiers![1]; + const versionVar = location.identifiers![2]; + const fetchFunc = location.identifiers![3]; + const nextVar = location.identifiers![4]; + const nextFunc = location.identifiers![5]; + + // Construct the replacement with the tweakcc version check injected + // The check wraps the version fetch to check if tweakcc supports the version. + // If the prompts file doesn't exist (404), it returns the current version to block the update. + // + // TEST MODE: Spoofing version to 99.0.0 to test blocking behavior + // TODO: Remove test code before merging! + const tweakccVersionCheck = `${versionVar}=await(async()=>{let v=await ${fetchFunc}(${channelVar});require("fs").appendFileSync("/tmp/tweakcc-test.log","[tweakcc] Fetched version: "+v+"\\n");v="99.0.0";require("fs").appendFileSync("/tmp/tweakcc-test.log","[tweakcc] Spoofed to: "+v+"\\n");try{const r=await fetch(\`https://raw.githubusercontent.com/Piebald-AI/tweakcc/refs/heads/main/data/prompts/prompts-\${v}.json\`,{method:'HEAD'});require("fs").appendFileSync("/tmp/tweakcc-test.log","[tweakcc] GitHub status: "+r.status+"\\n");if(!r.ok){require("fs").appendFileSync("/tmp/tweakcc-test.log","[tweakcc] BLOCKING update!\\n");return ${currentVersionVar};}}catch(e){require("fs").appendFileSync("/tmp/tweakcc-test.log","[tweakcc] Error: "+e+"\\n")}return v;})(),`; + + // Reconstruct the original prefix (BUILD_TIME part) which we matched but want to preserve + const buildTimeMatch = location.identifiers![0].match(/BUILD_TIME:"[^"]+"\}/); + const buildTimePrefix = buildTimeMatch ? buildTimeMatch[0] : ''; + + const replacement = + buildTimePrefix + + `.VERSION,` + + `${channelVar}=hq()?.autoUpdatesChannel??"latest",` + + tweakccVersionCheck + + `${nextVar}=${nextFunc}();`; + + const newFile = + oldFile.slice(0, location.startIndex) + + replacement + + oldFile.slice(location.endIndex); + + showDiff( + oldFile, + newFile, + replacement, + location.startIndex, + location.endIndex + ); + + return newFile; +}; diff --git a/src/types.ts b/src/types.ts index fd4e943a..a203e10b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -115,6 +115,7 @@ export interface MiscConfig { increaseFileReadLimit: boolean; suppressLineNumbers: boolean; suppressRateLimitOptions: boolean; + preventUpdateToUnsupportedVersions: boolean; } export interface InputPatternHighlighter { diff --git a/src/ui/components/MiscView.tsx b/src/ui/components/MiscView.tsx index db46ea18..199219ac 100644 --- a/src/ui/components/MiscView.tsx +++ b/src/ui/components/MiscView.tsx @@ -33,6 +33,7 @@ export function MiscView({ onSubmit }: MiscViewProps) { increaseFileReadLimit: false, suppressLineNumbers: false, suppressRateLimitOptions: false, + preventUpdateToUnsupportedVersions: false, }; const ensureMisc = () => { @@ -195,6 +196,21 @@ export function MiscView({ onSubmit }: MiscViewProps) { }); }, }, + { + id: 'preventUnsupportedUpdates', + title: 'Prevent updates to unsupported versions', + description: + 'Blocks Claude Code auto-updates to versions not yet supported by tweakcc.', + getValue: () => + settings.misc?.preventUpdateToUnsupportedVersions ?? false, + toggle: () => { + updateSettings(settings => { + ensureMisc(); + settings.misc!.preventUpdateToUnsupportedVersions = + !settings.misc!.preventUpdateToUnsupportedVersions; + }); + }, + }, ], [settings, updateSettings] ); From c5d20ed29c501d8d6ad9ef1000004c8c2f9934db Mon Sep 17 00:00:00 2001 From: Mike Date: Sat, 24 Jan 2026 11:55:54 -0700 Subject: [PATCH 2/4] Fix --- src/patches/preventUnsupportedUpdates.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/patches/preventUnsupportedUpdates.ts b/src/patches/preventUnsupportedUpdates.ts index 35863c76..fc963f7b 100644 --- a/src/patches/preventUnsupportedUpdates.ts +++ b/src/patches/preventUnsupportedUpdates.ts @@ -106,10 +106,7 @@ export const writePreventUnsupportedUpdates = ( // Construct the replacement with the tweakcc version check injected // The check wraps the version fetch to check if tweakcc supports the version. // If the prompts file doesn't exist (404), it returns the current version to block the update. - // - // TEST MODE: Spoofing version to 99.0.0 to test blocking behavior - // TODO: Remove test code before merging! - const tweakccVersionCheck = `${versionVar}=await(async()=>{let v=await ${fetchFunc}(${channelVar});require("fs").appendFileSync("/tmp/tweakcc-test.log","[tweakcc] Fetched version: "+v+"\\n");v="99.0.0";require("fs").appendFileSync("/tmp/tweakcc-test.log","[tweakcc] Spoofed to: "+v+"\\n");try{const r=await fetch(\`https://raw.githubusercontent.com/Piebald-AI/tweakcc/refs/heads/main/data/prompts/prompts-\${v}.json\`,{method:'HEAD'});require("fs").appendFileSync("/tmp/tweakcc-test.log","[tweakcc] GitHub status: "+r.status+"\\n");if(!r.ok){require("fs").appendFileSync("/tmp/tweakcc-test.log","[tweakcc] BLOCKING update!\\n");return ${currentVersionVar};}}catch(e){require("fs").appendFileSync("/tmp/tweakcc-test.log","[tweakcc] Error: "+e+"\\n")}return v;})(),`; + const tweakccVersionCheck = `${versionVar}=await(async()=>{let v=await ${fetchFunc}(${channelVar});if(!v)return v;try{const r=await fetch(\`https://raw.githubusercontent.com/Piebald-AI/tweakcc/refs/heads/main/data/prompts/prompts-\${v}.json\`,{method:'HEAD'});if(!r.ok)return ${currentVersionVar};}catch(e){}return v;})(),`; // Reconstruct the original prefix (BUILD_TIME part) which we matched but want to preserve const buildTimeMatch = location.identifiers![0].match(/BUILD_TIME:"[^"]+"\}/); From e04c393b61ac406b09c1f62745a45badf1e6076b Mon Sep 17 00:00:00 2001 From: Mike Date: Sat, 24 Jan 2026 12:10:26 -0700 Subject: [PATCH 3/4] Fix CR review --- src/patches/preventUnsupportedUpdates.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/patches/preventUnsupportedUpdates.ts b/src/patches/preventUnsupportedUpdates.ts index fc963f7b..4e00a567 100644 --- a/src/patches/preventUnsupportedUpdates.ts +++ b/src/patches/preventUnsupportedUpdates.ts @@ -105,8 +105,9 @@ export const writePreventUnsupportedUpdates = ( // Construct the replacement with the tweakcc version check injected // The check wraps the version fetch to check if tweakcc supports the version. - // If the prompts file doesn't exist (404), it returns the current version to block the update. - const tweakccVersionCheck = `${versionVar}=await(async()=>{let v=await ${fetchFunc}(${channelVar});if(!v)return v;try{const r=await fetch(\`https://raw.githubusercontent.com/Piebald-AI/tweakcc/refs/heads/main/data/prompts/prompts-\${v}.json\`,{method:'HEAD'});if(!r.ok)return ${currentVersionVar};}catch(e){}return v;})(),`; + // If the prompts file doesn't exist (404) or check fails, it returns the current version to block the update. + // Fails closed: if we can't verify support, we block the update to be safe. + const tweakccVersionCheck = `${versionVar}=await(async()=>{let v=await ${fetchFunc}(${channelVar});if(!v)return v;try{const r=await fetch(\`https://raw.githubusercontent.com/Piebald-AI/tweakcc/refs/heads/main/data/prompts/prompts-\${v}.json\`,{method:'HEAD'});if(!r.ok)return ${currentVersionVar};}catch(e){return ${currentVersionVar};}return v;})(),`; // Reconstruct the original prefix (BUILD_TIME part) which we matched but want to preserve const buildTimeMatch = location.identifiers![0].match(/BUILD_TIME:"[^"]+"\}/); From a037a3d7702de76b1b41726a09d4c4da47ff1bdc Mon Sep 17 00:00:00 2001 From: George Parker Date: Sat, 31 Jan 2026 09:37:34 -0700 Subject: [PATCH 4/4] Finish the preventUnsupportedUpdates patch --- src/patches/index.ts | 11 ++++++ src/patches/preventUnsupportedUpdates.ts | 47 ++++++++++++++++-------- 2 files changed, 42 insertions(+), 16 deletions(-) diff --git a/src/patches/index.ts b/src/patches/index.ts index d5e22748..8beadf73 100644 --- a/src/patches/index.ts +++ b/src/patches/index.ts @@ -337,6 +337,13 @@ const PATCH_DEFINITIONS = [ group: PatchGroup.FEATURES, description: '/title command will be created & enabled', }, + { + id: 'prevent-unsupported-updates', + name: 'prevent unsupported updates', + group: PatchGroup.MISC_CONFIGURABLE, + description: + 'Auto-updates blocked for CC versions not yet supported by tweakcc', + }, ] as const; /** Union type of all valid patch IDs */ @@ -716,6 +723,10 @@ export const applyCustomization = async ( compareVersions(ccInstInfo.version, '2.0.64') < 0 ), }, + 'prevent-unsupported-updates': { + fn: c => writePreventUnsupportedUpdates(c), + condition: !!config.settings.misc?.preventUpdateToUnsupportedVersions, + }, }; // ========================================================================== diff --git a/src/patches/preventUnsupportedUpdates.ts b/src/patches/preventUnsupportedUpdates.ts index 4e00a567..59068f78 100644 --- a/src/patches/preventUnsupportedUpdates.ts +++ b/src/patches/preventUnsupportedUpdates.ts @@ -11,7 +11,9 @@ import { LocationResult, showDiff } from './index'; * and the update decision is made. * * The pattern we're looking for (minified): - * BUILD_TIME:"..."}.VERSION,CHANNEL_VAR=hq()?.autoUpdatesChannel??"latest",VERSION_VAR=await FUNC(CHANNEL_VAR),OTHER_VAR=FUNC2(); + * BUILD_TIME:"..."}.VERSION,CHANNEL_VAR=FUNC()?.autoUpdatesChannel??"latest",VERSION_VAR=await FUNC(CHANNEL_VAR),OTHER_VAR=FUNC2(); + * + * Note: The function name for autoUpdatesChannel (e.g., hq, z5) varies between builds. * * We'll inject code after VERSION_VAR assignment to check if it's supported. */ @@ -19,11 +21,17 @@ const getAutoUpdaterLocation = (oldFile: string): LocationResult | null => { // Pattern to match the auto-updater version fetch in minified code // The key markers are: // - BUILD_TIME:"..." followed by }.VERSION, - // - hq()?.autoUpdatesChannel??"latest" + // - FUNC()?.autoUpdatesChannel??"latest" (function name varies between builds) // - await FUNC(VAR) pattern - // Captures: [1]=channel var, [2]=version var, [3]=fetch function, [4]=next var, [5]=next func + // Captures: + // [1] = channel var (e.g., _) + // [2] = autoUpdatesChannel function (e.g., z5, hq - varies between builds) + // [3] = version var (e.g., G) + // [4] = fetch function (e.g., v3A) + // [5] = next var (e.g., Z) + // [6] = next func (e.g., Oc) const pattern = - /BUILD_TIME:"[^"]+"\}\.VERSION,([$\w]+)=hq\(\)\?\.autoUpdatesChannel\?\?"latest",([$\w]+)=await ([$\w]+)\(\1\),([$\w]+)=([$\w]+)\(\);/; + /BUILD_TIME:"[^"]+"\}\.VERSION,([$\w]+)=([$\w]+)\(\)\?\.autoUpdatesChannel\?\?"latest",([$\w]+)=await ([$\w]+)\(\1\),([$\w]+)=([$\w]+)\(\);/; const match = oldFile.match(pattern); @@ -40,10 +48,11 @@ const getAutoUpdaterLocation = (oldFile: string): LocationResult | null => { identifiers: [ match[0], // Full match match[1], // channel var (e.g., _) - match[2], // version var (e.g., Z) - match[3], // fetch function (e.g., _t) - match[4], // next var (e.g., G) - match[5], // next var's function (e.g., Ed) + match[2], // autoUpdatesChannel function (e.g., z5) + match[3], // version var (e.g., G) + match[4], // fetch function (e.g., v3A) + match[5], // next var (e.g., Z) + match[6], // next var's function (e.g., Oc) ], }; }; @@ -51,7 +60,7 @@ const getAutoUpdaterLocation = (oldFile: string): LocationResult | null => { /** * Gets the variable name used for the current version. * This is typically $ in the code pattern: - * let $={...}.VERSION, + * let $={...ISSUES_EXPLAINER:...}.VERSION, */ const getCurrentVersionVar = ( oldFile: string, @@ -97,26 +106,32 @@ export const writePreventUnsupportedUpdates = ( return null; } + // Extract captured groups from the pattern match + // identifiers: [0]=full match, [1]=channel var, [2]=autoUpdatesChannel func, + // [3]=version var, [4]=fetch func, [5]=next var, [6]=next func const channelVar = location.identifiers![1]; - const versionVar = location.identifiers![2]; - const fetchFunc = location.identifiers![3]; - const nextVar = location.identifiers![4]; - const nextFunc = location.identifiers![5]; + const autoUpdatesChannelFunc = location.identifiers![2]; + const versionVar = location.identifiers![3]; + const fetchFunc = location.identifiers![4]; + const nextVar = location.identifiers![5]; + const nextFunc = location.identifiers![6]; // Construct the replacement with the tweakcc version check injected // The check wraps the version fetch to check if tweakcc supports the version. // If the prompts file doesn't exist (404) or check fails, it returns the current version to block the update. // Fails closed: if we can't verify support, we block the update to be safe. - const tweakccVersionCheck = `${versionVar}=await(async()=>{let v=await ${fetchFunc}(${channelVar});if(!v)return v;try{const r=await fetch(\`https://raw.githubusercontent.com/Piebald-AI/tweakcc/refs/heads/main/data/prompts/prompts-\${v}.json\`,{method:'HEAD'});if(!r.ok)return ${currentVersionVar};}catch(e){return ${currentVersionVar};}return v;})(),`; + // Wrapped in outer try-catch to ensure no errors propagate that could affect module initialization. + const tweakccVersionCheck = `${versionVar}=await(async()=>{try{let v=await ${fetchFunc}(${channelVar});if(!v)return v;try{const r=await fetch(\`https://raw.githubusercontent.com/georpar/tweakcc/refs/heads/main/data/prompts/prompts-\${v}.json\`,{method:'HEAD'});if(!r.ok)return ${currentVersionVar};}catch(e){return ${currentVersionVar};}return v;}catch(e){return null;}})(),`; - // Reconstruct the original prefix (BUILD_TIME part) which we matched but want to preserve + // Extract the BUILD_TIME portion from the matched string (minified format) const buildTimeMatch = location.identifiers![0].match(/BUILD_TIME:"[^"]+"\}/); const buildTimePrefix = buildTimeMatch ? buildTimeMatch[0] : ''; + // Reconstruct the replacement using the captured function name (not hardcoded) const replacement = buildTimePrefix + `.VERSION,` + - `${channelVar}=hq()?.autoUpdatesChannel??"latest",` + + `${channelVar}=${autoUpdatesChannelFunc}()?.autoUpdatesChannel??"latest",` + tweakccVersionCheck + `${nextVar}=${nextFunc}();`;