Skip to content

Instantly share code, notes, and snippets.

@heeen
Created January 12, 2026 23:03
Show Gist options
  • Select an option

  • Save heeen/b6ced51aeac9da1b315776f38752c4ff to your computer and use it in GitHub Desktop.

Select an option

Save heeen/b6ced51aeac9da1b315776f38752c4ff to your computer and use it in GitHub Desktop.
Patch for Claude Code issue #6237 - fixes freeze on /dev/* device paths
#!/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