Skip to content

Commit f5b19fc

Browse files
authored
Fix React var and AGENTS.md patching 2.1.62 (#563)
1 parent 0e97449 commit f5b19fc

4 files changed

Lines changed: 56 additions & 126 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## Unreleased
99

10+
- Fix React var and AGENTS.md patching 2.1.62 (#563) - @bl-ue
11+
1012
## [v4.0.9](https://github.com/Piebald-AI/tweakcc/releases/tag/v4.0.9) - 2026-02-25
1113

1214
- Add Nix support (#548) - @signadou

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ $ pnpm dlx tweakcc
161161

162162
tweakcc works by patching Claude Code's minified `cli.js` file, reading customizations from `~/.tweakcc/config.json`. For npm-based installations `cli.js` is modified directly, but for native installations it's extracted from the binary using [node-lief](https://github.com/Piebald-AI/node-lief), patched, and then the binary is repacked. When you update your Claude Code installation, your customizations will be overwritten, but they're remembered in your configuration file, so they can be reapplied by just running `npx tweakcc --apply`.
163163

164-
tweakcc is verified to work with Claude Code **2.1.55.** In newer or earlier versions various patches might not work. However, if we have the [system prompts for your version](https://github.com/Piebald-AI/tweakcc/tree/main/data/prompts) then system prompt patching is guaranteed to work with that version, even if it's significantly different from the verified CC version—the version number stated above is only relevant for the non-system-prompt patches. We get the latest system prompts within minutes of each new CC release, so unless you're using a CC version older than 2.0.14, your version is supported.
164+
tweakcc is verified to work with Claude Code **2.1.62.** In newer or earlier versions various patches might not work. However, if we have the [system prompts for your version](https://github.com/Piebald-AI/tweakcc/tree/main/data/prompts) then system prompt patching is guaranteed to work with that version, even if it's significantly different from the verified CC version—the version number stated above is only relevant for the non-system-prompt patches. We get the latest system prompts within minutes of each new CC release, so unless you're using a CC version older than 2.0.14, your version is supported.
165165

166166
You can also create custom patches using tweakcc without having to fork it or open a PR. [`tweakcc adhoc-patch`](#cli-commands) supports using custom scripts that work with native and npm-based installs and that automatically detect your Claude Code installation.
167167

src/patches/agentsMd.ts

Lines changed: 40 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -7,82 +7,30 @@ import { showDiff } from './index';
77
* filenames (e.g., AGENTS.md).
88
*
99
* This finds the function that reads CLAUDE.md files and modifies it to:
10-
* 1. First try the original CLAUDE.md path
11-
* 2. If not found, try each alternative name in order
10+
* 1. Add a `didReroute` parameter to the function
11+
* 2. Right before the `return {` statement, check if the path ends with
12+
* CLAUDE.md and try alternative names (unless didReroute is true)
13+
* 3. Recursive calls pass didReroute=true to avoid infinite loops
1214
*
13-
* CC 1.0.24:
15+
* CC 2.1.62 (approx. by Claude):
1416
* ```diff
15-
* function gE2(A, B) {
16-
* try {
17-
* if (f1().existsSync(A)) {
18-
* if (!f1().statSync(A).isFile()) return null;
19-
* let I = f1().readFileSync(A, { encoding: "utf-8" });
20-
* return { path: A, type: B, content: I };
21-
* + } else if (A.endsWith("/CLAUDE.md") || A.endsWith("\\CLAUDE.md")) {
22-
* + for (let alt of ["AGENTS.md", "GEMINI.md", "QWEN.md"]) {
23-
* + let altPath = A.slice(0, -9) + alt;
24-
* + if (f1().existsSync(altPath) && f1().statSync(altPath).isFile())
25-
* + return gE2(altPath, B);
26-
* + }
27-
* + }
28-
* } catch (Q) {
29-
* if (Q instanceof Error && Q.message.includes("EACCES"))
30-
* N1("tengu_claude_md_permission_error", {
31-
* is_access_error: 1,
32-
* has_home_dir: A.includes(z4()) ? 1 : 0,
33-
* });
34-
* }
35-
* return null;
36-
* }
37-
* ```
38-
*
39-
* CC 2.0.0:
40-
* ```
41-
* function q8B(A, B) {
42-
* try {
43-
* if (C1().existsSync(A)) {
44-
* if (!C1().statSync(A).isFile()) return null;
45-
* let Z = C1().readFileSync(A, { encoding: "utf-8" });
46-
* return { path: A, type: B, content: Z };
47-
* + } else if (A.endsWith("/CLAUDE.md") || A.endsWith("\\CLAUDE.md")) {
48-
* + for (let alt of ["AGENTS.md", "GEMINI.md", "QWEN.md"]) {
49-
* + let altPath = A.slice(0, -9) + alt;
50-
* + if (C1().existsSync(altPath) && C1().statSync(altPath).isFile())
51-
* + return q8B(altPath, B);
52-
* + }
53-
* }
54-
* } catch (Q) {
55-
* if (Q instanceof Error && Q.message.includes("EACCES"))
56-
* B1("tengu_claude_md_permission_error", {
57-
* is_access_error: 1,
58-
* has_home_dir: A.includes(p2()) ? 1 : 0,
59-
* });
60-
* }
61-
* return null;
62-
* }
63-
* ```
64-
*
65-
* CC 2.1.29:
66-
* ```
67-
* function _t7(A, q) {
17+
* -function _t7(A, q) {
18+
* +function _t7(A, q, didReroute) {
6819
* try {
6920
* let K = x1();
70-
* - if (!K.existsSync(A) || !K.statSync(A).isFile()) return null;
71-
* + if (!K.existsSync(A) || !K.statSync(A).isFile()) {
72-
* + if (A.endsWith("/CLAUDE.md") || A.endsWith("\\CLAUDE.md")) {
73-
* + for (let alt of ["AGENTS.md", "GEMINI.md", "QWEN.md"]) {
74-
* + let altPath = A.slice(0, -9) + alt;
75-
* + if (K.existsSync(altPath) && K.statSync(altPath).isFile())
76-
* + return _t7(altPath, q);
77-
* + }
78-
* + }
79-
* + return null;
80-
* + }
21+
* if (!K.existsSync(A) || !K.statSync(A).isFile()) return null;
8122
* let Y = UL9(A).toLowerCase();
8223
* if (Y && !dL9.has(Y))
8324
* return (I(`Skipping non-text file in @include: ${A}`), null);
8425
* let z = K.readFileSync(A, { encoding: "utf-8" }),
8526
* { content: w, paths: H } = cL9(z);
27+
* + if (!didReroute && (A.endsWith("/CLAUDE.md") || A.endsWith("\\CLAUDE.md"))) {
28+
* + for (let alt of ["AGENTS.md", "GEMINI.md", "QWEN.md"]) {
29+
* + let altPath = A.slice(0, -9) + alt;
30+
* + if (K.existsSync(altPath) && K.statSync(altPath).isFile())
31+
* + return _t7(altPath, q, true);
32+
* + }
33+
* + }
8634
* return { path: A, type: q, content: w, globs: H };
8735
* } catch (K) {
8836
* if (K instanceof Error && K.message.includes("EACCES"))
@@ -99,85 +47,57 @@ export const writeAgentsMd = (
9947
file: string,
10048
altNames: string[]
10149
): string | null => {
102-
// Step 1: Find the function that handles CLAUDE.md file reading
103-
// Pattern: function xyz(a, b) { ...... return { ... path: ..., content: ... } }
10450
const funcPattern =
105-
/function ([$\w]+)\(([$\w]+),([$\w]+)\)(?:.|\n){0,500}Skipping non-text file in @include(?:.|\n){0,500}\{path:[$\w]+,.{0,20}?content:[$\w]+/;
51+
/(function ([$\w]+)\(([$\w]+),([^)]+?))\)(?:.|\n){0,500}Skipping non-text file in @include(?:.|\n){0,500}return\{path:[$\w]+,.{0,20}?content:[$\w]+/;
10652

10753
const funcMatch = file.match(funcPattern);
108-
10954
if (!funcMatch || funcMatch.index === undefined) {
11055
console.error('patch: agentsMd: failed to find CLAUDE.md reading function');
11156
return null;
11257
}
113-
114-
const functionName = funcMatch[1];
115-
const firstParam = funcMatch[2];
116-
const secondParam = funcMatch[3];
58+
const upToFuncParamsClosingParen = funcMatch[1];
59+
const functionName = funcMatch[2];
60+
const firstParam = funcMatch[3];
61+
const restParams = funcMatch[4];
11762
const funcStart = funcMatch.index;
11863

119-
// Step 2: Find the fs expression used in the function
120-
// Search within the matched region for fsVar.readFileSync etc. or fsVar().readFileSync etc.
12164
const fsPattern = /([$\w]+(?:\(\))?)\.(?:readFileSync|existsSync|statSync)/;
12265
const fsMatch = funcMatch[0].match(fsPattern);
123-
12466
if (!fsMatch) {
12567
console.error('patch: agentsMd: failed to find fs expression in function');
12668
return null;
12769
}
128-
12970
const fsExpr = fsMatch[1];
13071

131-
// Prepare the alternative names as JSON
13272
const altNamesJson = JSON.stringify(altNames);
13373

134-
// Build the injection code
135-
const buildInjection = () =>
136-
`if(${firstParam}.endsWith("/CLAUDE.md")||${firstParam}.endsWith("\\\\CLAUDE.md")){for(let alt of ${altNamesJson}){let altPath=${firstParam}.slice(0,-9)+alt;if(${fsExpr}.existsSync(altPath)&&${fsExpr}.statSync(altPath).isFile())return ${functionName}(altPath,${secondParam});}}return null;`;
137-
138-
// Step 3: Try the primary pattern (CC 2.1.29)
139-
// Pattern: if(!fs.existsSync(path)||!fs.statSync(path).isFile())return null;
140-
const primaryPattern =
141-
/(if\(![$\w]+\.existsSync\([$\w]+\)\|\|![$\w]+\.statSync\([$\w]+\)\.isFile\(\)\))return null;/;
142-
const primaryMatch = file.slice(funcStart).match(primaryPattern);
143-
144-
if (primaryMatch && primaryMatch.index !== undefined) {
145-
const injection = buildInjection();
146-
const replacement = primaryMatch[1] + '{' + injection + '}';
74+
const sigIndex = funcStart + upToFuncParamsClosingParen.length;
75+
let newFile = file.slice(0, sigIndex) + ',didReroute' + file.slice(sigIndex);
14776

148-
const startIndex = funcStart + primaryMatch.index;
149-
const endIndex = startIndex + primaryMatch[0].length;
77+
showDiff(file, newFile, ',didReroute', sigIndex, sigIndex);
15078

151-
const newFile =
152-
file.slice(0, startIndex) + replacement + file.slice(endIndex);
79+
// Step 2: Inject rerouting code right before the `return {`
80+
const returnPattern = /return\{path:[$\w]+,.{0,20}?content:[$\w]+/;
81+
const returnMatch = newFile.slice(funcStart).match(returnPattern);
15382

154-
showDiff(file, newFile, replacement, startIndex, endIndex);
155-
156-
return newFile;
83+
if (!returnMatch || returnMatch.index === undefined) {
84+
console.error(
85+
'patch: agentsMd: failed to find return statement for injection'
86+
);
87+
return null;
15788
}
15889

159-
// Step 4: Try the fallback pattern (uses }catch or }}catch)
160-
// Pattern: }catch or }}catch
161-
const fallbackPattern = /(\})(}catch)/;
162-
const fallbackMatch = file.slice(funcStart).match(fallbackPattern);
163-
164-
if (fallbackMatch && fallbackMatch.index !== undefined) {
165-
const injection = `else if(${firstParam}.endsWith("/CLAUDE.md")||${firstParam}.endsWith("\\\\CLAUDE.md")){for(let alt of ${altNamesJson}){let altPath=${firstParam}.slice(0,-9)+alt;if(${fsExpr}.existsSync(altPath)&&${fsExpr}.statSync(altPath).isFile())return ${functionName}(altPath,${secondParam});}}`;
166-
const replacement = fallbackMatch[1] + injection + fallbackMatch[2];
90+
const injection = `if(!didReroute&&(${firstParam}.endsWith("/CLAUDE.md")||${firstParam}.endsWith("\\\\CLAUDE.md"))){for(let alt of ${altNamesJson}){let altPath=${firstParam}.slice(0,-9)+alt;if(${fsExpr}.existsSync(altPath)&&${fsExpr}.statSync(altPath).isFile())return ${functionName}(altPath,${restParams},true);}}`;
16791

168-
const startIndex = funcStart + fallbackMatch.index;
169-
const endIndex = startIndex + fallbackMatch[0].length;
92+
const returnStart = funcStart + returnMatch.index;
93+
const replacement = injection + returnMatch[0];
17094

171-
const newFile =
172-
file.slice(0, startIndex) + replacement + file.slice(endIndex);
95+
newFile =
96+
newFile.slice(0, returnStart) +
97+
replacement +
98+
newFile.slice(returnStart + returnMatch[0].length);
17399

174-
showDiff(file, newFile, replacement, startIndex, endIndex);
175-
176-
return newFile;
177-
}
100+
showDiff(file, newFile, replacement, returnStart, returnStart);
178101

179-
console.error(
180-
'patch: agentsMd: failed to find insertion point in function body'
181-
);
182-
return null;
102+
return newFile;
183103
};

src/patches/helpers.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,19 @@ export const getModuleLoaderFunction = (
4141
}
4242

4343
// NPM bundles: var T=(H,$,A)=>{ at the start
44-
const firstChunk = fileContents.slice(0, 1000);
45-
const pattern = /(?:var |,)([$\w]+)=\([$\w]+,[$\w]+,[$\w]+\)=>\{/;
46-
const match = firstChunk.match(pattern);
47-
if (match) {
48-
return match[1];
44+
// In newer versions there are more than one, and the one with the shortest name
45+
// is the most common one and therefore the correct one.
46+
const firstChunk = fileContents.slice(0, 10000);
47+
const pattern = /(?:var |,)([$\w]+)=\([$\w]+,[$\w]+,[$\w]+\)=>\{/g;
48+
const matches = Array.from(firstChunk.matchAll(pattern));
49+
if (matches.length > 0) {
50+
let shortest = matches[0][1];
51+
for (const m of matches) {
52+
if (m[1].length < shortest.length) {
53+
shortest = m[1];
54+
}
55+
}
56+
return shortest;
4957
}
5058

5159
console.log(

0 commit comments

Comments
 (0)