Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/defaultSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -755,6 +755,7 @@ export const DEFAULT_SETTINGS: Settings = {
increaseFileReadLimit: false,
suppressLineNumbers: true,
suppressRateLimitOptions: false,
preventUpdateToUnsupportedVersions: false,
},
toolsets: [],
defaultToolset: null,
Expand Down
6 changes: 6 additions & 0 deletions src/patches/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ import {
restoreClijsFromBackup,
} from '../installationBackup';
import { compareVersions } from '../systemPromptSync';
import { writePreventUnsupportedUpdates } from './preventUnsupportedUpdates';

export interface LocationResult {
startIndex: number;
Expand Down Expand Up @@ -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
Expand Down
136 changes: 136 additions & 0 deletions src/patches/preventUnsupportedUpdates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// 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.
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:"[^"]+"\}/);
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;
};
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export interface MiscConfig {
increaseFileReadLimit: boolean;
suppressLineNumbers: boolean;
suppressRateLimitOptions: boolean;
preventUpdateToUnsupportedVersions: boolean;
}

export interface InputPatternHighlighter {
Expand Down
16 changes: 16 additions & 0 deletions src/ui/components/MiscView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export function MiscView({ onSubmit }: MiscViewProps) {
increaseFileReadLimit: false,
suppressLineNumbers: false,
suppressRateLimitOptions: false,
preventUpdateToUnsupportedVersions: false,
};

const ensureMisc = () => {
Expand Down Expand Up @@ -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]
);
Expand Down