Skip to content
Open
Show file tree
Hide file tree
Changes from all 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 Notice
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
To test auto code review in PRs
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Committing test placeholders like Notice to the repository root clutters the project structure. Such files should be removed before merging. If the intent is to add a legal notice, use the standard NOTICE filename and include the appropriate attribution text.

173 changes: 173 additions & 0 deletions windowsPaths.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import memoize from 'lodash-es/memoize.js'
import * as path from 'path'
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Import the fs module to enable efficient file system checks without spawning external processes.

Suggested change
import * as path from 'path'
import * as fs from 'fs'
import * as path from 'path'

import * as pathWin32 from 'path/win32'
import { getCwd } from './cwd.js'
import { logForDebugging } from './debug.js'
import { execSync_DEPRECATED } from './execSyncWrapper.js'
import { memoizeWithLRU } from './memoize.js'
import { getPlatform } from './platform.js'
Comment on lines +1 to +8
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Add existsSync to the imports to support a more efficient and secure path existence check in checkPathExists.

Suggested change
import memoize from 'lodash-es/memoize.js'
import * as path from 'path'
import * as pathWin32 from 'path/win32'
import { getCwd } from './cwd.js'
import { logForDebugging } from './debug.js'
import { execSync_DEPRECATED } from './execSyncWrapper.js'
import { memoizeWithLRU } from './memoize.js'
import { getPlatform } from './platform.js'
import memoize from 'lodash-es/memoize.js'
import { existsSync } from 'node:fs'
import * as path from 'path'
import * as pathWin32 from 'path/win32'
import { getCwd } from './cwd.js'
import { logForDebugging } from './debug.js'
import { execSync_DEPRECATED } from './execSyncWrapper.js'
import { memoizeWithLRU } from './memoize.js'
import { getPlatform } from './platform.js'


/**
* Check if a file or directory exists on Windows using the dir command
* @param path - The path to check
* @returns true if the path exists, false otherwise
*/
function checkPathExists(path: string): boolean {
try {
execSync_DEPRECATED(`dir "${path}"`, { stdio: 'pipe' })
return true
} catch {
return false
}
}
Comment on lines +15 to +22
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using execSync to run the dir command is an expensive operation because it spawns a new shell process. Using the built-in fs.existsSync is significantly faster and more reliable.

function checkPathExists(path: string): boolean {
  return fs.existsSync(path)
}

Comment on lines +15 to +22
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The checkPathExists function is inefficient and insecure. Spawning a shell process (dir) to check for file existence is significantly slower than using Node.js built-in functions and is vulnerable to command injection if the path contains double quotes (e.g., " & calc). Additionally, the parameter name path shadows the imported path module. Use existsSync from node:fs instead.

function checkPathExists(filePath: string): boolean {
  return existsSync(filePath)
}


/**
* Find an executable using where.exe on Windows
* @param executable - The name of the executable to find
* @returns The path to the executable or null if not found
*/
function findExecutable(executable: string): string | null {
// For git, check common installation locations first
if (executable === 'git') {
const defaultLocations = [
// check 64 bit before 32 bit
'C:\\Program Files\\Git\\cmd\\git.exe',
'C:\\Program Files (x86)\\Git\\cmd\\git.exe',
// intentionally don't look for C:\Program Files\Git\mingw64\bin\git.exe
// because that directory is the "raw" tools with no environment setup
]

for (const location of defaultLocations) {
if (checkPathExists(location)) {
return location
}
}
}

// Fall back to where.exe
try {
const result = execSync_DEPRECATED(`where.exe ${executable}`, {
stdio: 'pipe',
encoding: 'utf8',
}).trim()
Comment on lines +49 to +52
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

Potential command injection vulnerability. The executable variable is interpolated directly into a shell command string. If the input is not strictly controlled, an attacker could execute arbitrary commands by including shell metacharacters. Consider using execFileSync or another method that avoids shell execution.


// SECURITY: Filter out any results from the current directory
// to prevent executing malicious git.bat/cmd/exe files
const paths = result.split('\r\n').filter(Boolean)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Splitting by \r\n specifically may fail if the environment returns only \n (LF). Using a regex like /\r?\n/ is more robust for handling different line ending conventions on Windows.

Suggested change
const paths = result.split('\r\n').filter(Boolean)
const paths = result.split(/\r?\n/).filter(Boolean)

const cwd = getCwd().toLowerCase()

for (const candidatePath of paths) {
// Normalize and compare paths to ensure we're not in current directory
const normalizedPath = path.resolve(candidatePath).toLowerCase()
const pathDir = path.dirname(normalizedPath).toLowerCase()

// Skip if the executable is in the current working directory
if (pathDir === cwd || normalizedPath.startsWith(cwd + path.sep)) {
logForDebugging(
`Skipping potentially malicious executable in current directory: ${candidatePath}`,
)
continue
}

// Return the first valid path that's not in the current directory
return candidatePath
}

return null
} catch {
return null
}
}

/**
* If Windows, set the SHELL environment variable to git-bash path.
* This is used by BashTool and Shell.ts for user shell commands.
* COMSPEC is left unchanged for system process execution.
*/
export function setShellIfWindows(): void {
if (getPlatform() === 'windows') {
const gitBashPath = findGitBashPath()
process.env.SHELL = gitBashPath
logForDebugging(`Using bash path: "${gitBashPath}"`)
}
}

/**
* Find the path where `bash.exe` included with git-bash exists, exiting the process if not found.
*/
export const findGitBashPath = memoize((): string => {
if (process.env.CLAUDE_CODE_GIT_BASH_PATH) {
if (checkPathExists(process.env.CLAUDE_CODE_GIT_BASH_PATH)) {
return process.env.CLAUDE_CODE_GIT_BASH_PATH
}
// biome-ignore lint/suspicious/noConsole:: intentional console output
console.error(
`Claude Code was unable to find CLAUDE_CODE_GIT_BASH_PATH path "${process.env.CLAUDE_CODE_GIT_BASH_PATH}"`,
)
// eslint-disable-next-line custom-rules/no-process-exit
process.exit(1)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Directly exiting the process in a utility function can make the library difficult to use in different contexts (e.g., tests or other applications). Consider throwing a descriptive error instead, allowing the caller to decide how to handle the failure.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using process.exit(1) in a utility function is discouraged as it terminates the entire application, making it difficult for callers to handle errors or for the code to be unit tested. Consider throwing an error instead.

Suggested change
process.exit(1)
throw new Error(`Claude Code was unable to find CLAUDE_CODE_GIT_BASH_PATH path "${process.env.CLAUDE_CODE_GIT_BASH_PATH}"`)

}

const gitPath = findExecutable('git')
if (gitPath) {
const bashPath = pathWin32.join(gitPath, '..', '..', 'bin', 'bash.exe')
if (checkPathExists(bashPath)) {
return bashPath
}
}

// biome-ignore lint/suspicious/noConsole:: intentional console output
console.error(
'Claude Code on Windows requires git-bash (https://git-scm.com/downloads/win). If installed but not in PATH, set environment variable pointing to your bash.exe, similar to: CLAUDE_CODE_GIT_BASH_PATH=C:\\Program Files\\Git\\bin\\bash.exe',
)
// eslint-disable-next-line custom-rules/no-process-exit
process.exit(1)
})

/** Convert a Windows path to a POSIX path using pure JS. */
export const windowsPathToPosixPath = memoizeWithLRU(
(windowsPath: string): string => {
// Handle UNC paths: \\server\share -> //server/share
if (windowsPath.startsWith('\\\\')) {
return windowsPath.replace(/\\/g, '/')
}
// Handle drive letter paths: C:\Users\foo -> /c/Users/foo
const match = windowsPath.match(/^([A-Za-z]):[/\\]/)
if (match) {
const driveLetter = match[1]!.toLowerCase()
return '/' + driveLetter + windowsPath.slice(2).replace(/\\/g, '/')
}
// Already POSIX or relative — just flip slashes
return windowsPath.replace(/\\/g, '/')
},
(p: string) => p,
500,
)

/** Convert a POSIX path to a Windows path using pure JS. */
export const posixPathToWindowsPath = memoizeWithLRU(
(posixPath: string): string => {
// Handle UNC paths: //server/share -> \\server\share
if (posixPath.startsWith('//')) {
return posixPath.replace(/\//g, '\\')
}
// Handle /cygdrive/c/... format
const cygdriveMatch = posixPath.match(/^\/cygdrive\/([A-Za-z])(\/|$)/)
if (cygdriveMatch) {
const driveLetter = cygdriveMatch[1]!.toUpperCase()
const rest = posixPath.slice(('/cygdrive/' + cygdriveMatch[1]).length)
return driveLetter + ':' + (rest || '\\').replace(/\//g, '\\')
}
// Handle /c/... format (MSYS2/Git Bash)
const driveMatch = posixPath.match(/^\/([A-Za-z])(\/|$)/)
if (driveMatch) {
const driveLetter = driveMatch[1]!.toUpperCase()
const rest = posixPath.slice(2)
return driveLetter + ':' + (rest || '\\').replace(/\//g, '\\')
}
// Already Windows or relative — just flip slashes
return posixPath.replace(/\//g, '\\')
},
(p: string) => p,
500,
)
13 changes: 13 additions & 0 deletions withResolvers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Polyfill for Promise.withResolvers() (ES2024, Node 22+).
* package.json declares "engines": { "node": ">=18.0.0" } so we can't use the native one.
*/
export function withResolvers<T>(): PromiseWithResolvers<T> {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Since this polyfill is for environments where Promise.withResolvers is not natively available, the PromiseWithResolvers type might also be missing from the global scope. Defining it locally ensures type safety and compatibility.

Suggested change
export function withResolvers<T>(): PromiseWithResolvers<T> {
export interface PromiseWithResolvers<T> {
promise: Promise<T>
resolve: (value: T | PromiseLike<T>) => void
reject: (reason?: any) => void
}
export function withResolvers<T>(): PromiseWithResolvers<T> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The type PromiseWithResolvers<T> is likely missing in the targeted Node environments (>=18). You should define the interface locally to ensure type safety and successful compilation.

Suggested change
export function withResolvers<T>(): PromiseWithResolvers<T> {
export interface PromiseWithResolvers<T> {
promise: Promise<T>
resolve: (value: T | PromiseLike<T>) => void
reject: (reason?: any) => void
}
export function withResolvers<T>(): PromiseWithResolvers<T> {

let resolve!: (value: T | PromiseLike<T>) => void
let reject!: (reason?: unknown) => void
const promise = new Promise<T>((res, rej) => {
resolve = res
reject = rej
})
return { promise, resolve, reject }
}
Loading