Created
January 12, 2026 23:03
-
-
Save heeen/b6ced51aeac9da1b315776f38752c4ff to your computer and use it in GitHub Desktop.
Patch for Claude Code issue #6237 - fixes freeze on /dev/* device paths
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env node | |
| /** | |
| * Patch Claude Code CLI to fix device file blocking issue | |
| * https://github.com/anthropics/claude-code/issues/6237 | |
| * | |
| * This patches the minified cli.js directly to skip /dev/* paths | |
| * when reading files extracted from bash output. | |
| * | |
| * Usage: | |
| * node patch-cli.js # Patch installed CLI | |
| * node patch-cli.js --dry-run # Show what would be changed | |
| * node patch-cli.js --restore # Restore from backup | |
| * node patch-cli.js /path/to/cli.js # Patch specific file | |
| */ | |
| const fs = require('fs'); | |
| const path = require('path'); | |
| const parser = require('@babel/parser'); | |
| const traverse = require('@babel/traverse').default; | |
| const generate = require('@babel/generator').default; | |
| const t = require('@babel/types'); | |
| // Find the installed CLI path | |
| function findCliPath() { | |
| const possiblePaths = [ | |
| // Common nvm paths | |
| process.env.HOME + '/.nvm/versions/node/' + process.version + '/lib/node_modules/@anthropic-ai/claude-code/cli.js', | |
| // Global npm | |
| '/usr/local/lib/node_modules/@anthropic-ai/claude-code/cli.js', | |
| '/usr/lib/node_modules/@anthropic-ai/claude-code/cli.js', | |
| // Try to find via npm root | |
| ]; | |
| for (const p of possiblePaths) { | |
| if (fs.existsSync(p)) { | |
| return p; | |
| } | |
| } | |
| // Try npm root -g | |
| try { | |
| const { execSync } = require('child_process'); | |
| const npmRoot = execSync('npm root -g', { encoding: 'utf8' }).trim(); | |
| const cliPath = path.join(npmRoot, '@anthropic-ai/claude-code/cli.js'); | |
| if (fs.existsSync(cliPath)) { | |
| return cliPath; | |
| } | |
| } catch (e) { | |
| // Ignore | |
| } | |
| return null; | |
| } | |
| function parseSource(source) { | |
| return parser.parse(source, { | |
| sourceType: 'module', | |
| plugins: ['jsx'], | |
| errorRecovery: true, | |
| }); | |
| } | |
| function findAndPatchBashPathExtraction(ast) { | |
| let patchCount = 0; | |
| let patchLocations = []; | |
| traverse(ast, { | |
| CallExpression(nodePath) { | |
| const callee = nodePath.node.callee; | |
| // Look for .then() calls | |
| if (callee.type !== 'MemberExpression' || | |
| callee.property?.name !== 'then') { | |
| return; | |
| } | |
| // Check if the object being .then()'d looks like our ctB call | |
| // ctB returns a promise, and the .then() callback processes file paths | |
| const thenArg = nodePath.node.arguments[0]; | |
| if (!thenArg || | |
| (thenArg.type !== 'ArrowFunctionExpression' && | |
| thenArg.type !== 'FunctionExpression')) { | |
| return; | |
| } | |
| // Look for M5.call inside this .then() callback | |
| let hasM5Call = false; | |
| let m5CallPath = null; | |
| traverse(thenArg, { | |
| CallExpression(innerPath) { | |
| const innerCallee = innerPath.node.callee; | |
| if (innerCallee.type === 'MemberExpression' && | |
| innerCallee.object?.name === 'M5' && | |
| innerCallee.property?.name === 'call') { | |
| // Check if first arg has file_path property | |
| const firstArg = innerPath.node.arguments[0]; | |
| if (firstArg?.type === 'ObjectExpression') { | |
| const hasFilePath = firstArg.properties.some(p => | |
| (p.key?.name === 'file_path' || p.key?.value === 'file_path') | |
| ); | |
| if (hasFilePath) { | |
| hasM5Call = true; | |
| m5CallPath = innerPath; | |
| } | |
| } | |
| } | |
| }, | |
| noScope: true, | |
| }, nodePath.scope); | |
| if (!hasM5Call || !m5CallPath) return; | |
| // Found the pattern! Now find the path variable name | |
| const firstArg = m5CallPath.node.arguments[0]; | |
| const filePathProp = firstArg.properties.find(p => | |
| p.key?.name === 'file_path' || p.key?.value === 'file_path' | |
| ); | |
| if (!filePathProp) return; | |
| const pathVar = filePathProp.value; | |
| const pathVarName = pathVar.name || '_A'; | |
| // Find the await expression and its statement | |
| let awaitParent = m5CallPath.findParent(p => p.isAwaitExpression()); | |
| if (!awaitParent) return; | |
| let exprStmt = awaitParent.findParent(p => p.isExpressionStatement()); | |
| if (!exprStmt) return; | |
| // Check if we already patched this (look for /dev/ check before) | |
| const container = exprStmt.container; | |
| const idx = exprStmt.key; | |
| if (typeof idx === 'number' && idx > 0) { | |
| const prevStmt = container[idx - 1]; | |
| if (prevStmt?.type === 'IfStatement') { | |
| const testCode = generate(prevStmt.test).code; | |
| if (testCode.includes('/dev/')) { | |
| // Already patched | |
| return; | |
| } | |
| } | |
| } | |
| // Create the device path check | |
| const checkCondition = t.callExpression( | |
| t.memberExpression(t.identifier(pathVarName), t.identifier('startsWith')), | |
| [t.stringLiteral('/dev/')] | |
| ); | |
| const ifBody = t.blockStatement([ | |
| t.continueStatement() | |
| ]); | |
| const ifStatement = t.ifStatement(checkCondition, ifBody); | |
| // Insert before the M5.call statement | |
| exprStmt.insertBefore(ifStatement); | |
| const loc = m5CallPath.node.loc?.start || {}; | |
| patchLocations.push({ | |
| line: loc.line, | |
| pathVar: pathVarName, | |
| }); | |
| patchCount++; | |
| }, | |
| }); | |
| return { patchCount, patchLocations }; | |
| } | |
| function main() { | |
| const args = process.argv.slice(2); | |
| const dryRun = args.includes('--dry-run'); | |
| const restore = args.includes('--restore'); | |
| // Find CLI path | |
| let cliPath = args.find(a => !a.startsWith('--') && a.endsWith('.js')); | |
| if (!cliPath) { | |
| cliPath = findCliPath(); | |
| } | |
| if (!cliPath) { | |
| console.error('Error: Could not find Claude Code CLI installation.'); | |
| console.error('Please provide the path: node patch-cli.js /path/to/cli.js'); | |
| process.exit(1); | |
| } | |
| console.log(`CLI path: ${cliPath}`); | |
| const backupPath = cliPath + '.backup'; | |
| if (restore) { | |
| if (!fs.existsSync(backupPath)) { | |
| console.error('Error: No backup file found at', backupPath); | |
| process.exit(1); | |
| } | |
| fs.copyFileSync(backupPath, cliPath); | |
| console.log('Restored from backup'); | |
| return; | |
| } | |
| // Read the CLI source | |
| console.log('Reading CLI source...'); | |
| const source = fs.readFileSync(cliPath, 'utf8'); | |
| console.log(`Source size: ${(source.length / 1024 / 1024).toFixed(1)}MB`); | |
| // Check if already patched | |
| if (source.includes('[dev-protect]') || | |
| /if\s*\([^)]*\.startsWith\s*\(\s*["']\/dev\/["']\s*\)\s*\)\s*\{\s*continue/.test(source)) { | |
| console.log('CLI appears to already be patched.'); | |
| process.exit(0); | |
| } | |
| // Parse | |
| console.log('Parsing AST...'); | |
| const ast = parseSource(source); | |
| // Find and patch | |
| console.log('Finding bash output path extraction code...'); | |
| const { patchCount, patchLocations } = findAndPatchBashPathExtraction(ast); | |
| if (patchCount === 0) { | |
| console.error('Error: Could not find the code pattern to patch.'); | |
| console.error('The CLI structure may have changed. Please report this issue.'); | |
| process.exit(1); | |
| } | |
| console.log(`Found ${patchCount} location(s) to patch:`); | |
| patchLocations.forEach(loc => { | |
| console.log(` Line ~${loc.line}: M5.call with path var '${loc.pathVar}'`); | |
| }); | |
| if (dryRun) { | |
| console.log('\n--dry-run: No changes made.'); | |
| return; | |
| } | |
| // Generate patched code | |
| console.log('Generating patched code...'); | |
| const output = generate(ast, { | |
| retainLines: false, | |
| compact: true, | |
| comments: false, | |
| }, source); | |
| // Create backup | |
| if (!fs.existsSync(backupPath)) { | |
| console.log(`Creating backup at ${backupPath}`); | |
| fs.copyFileSync(cliPath, backupPath); | |
| } | |
| // Write patched CLI | |
| console.log('Writing patched CLI...'); | |
| fs.writeFileSync(cliPath, output.code); | |
| console.log(`\nSuccess! Patched ${patchCount} location(s).`); | |
| console.log('The CLI will now skip /dev/* paths when reading bash output.'); | |
| console.log(`\nTo restore: node patch-cli.js --restore`); | |
| } | |
| main(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment