Skip to content
Merged
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
18 changes: 14 additions & 4 deletions src/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,10 +94,20 @@ export async function setup(
// (SessionStart in particular) can spawn and snapshot process.env.
if (feature('UDS_INBOX')) {
const m = await import('./utils/udsMessaging.js')
await m.startUdsMessaging(
messagingSocketPath ?? m.getDefaultUdsSocketPath(),
{ isExplicit: messagingSocketPath !== undefined },
)
try {
await m.startUdsMessaging(
messagingSocketPath ?? m.getDefaultUdsSocketPath(),
{ isExplicit: messagingSocketPath !== undefined },
)
} catch (error) {
logError(error)
console.error(
chalk.red(
`Error: Failed to start messaging socket (UDS_INBOX): ${errorMessage(error)}`,
),
)
process.exit(1)
}
}
}

Expand Down
49 changes: 39 additions & 10 deletions src/utils/__tests__/udsMessaging.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import {
MAX_UDS_INBOX_BYTES,
MAX_UDS_FRAME_BYTES,
MAX_UDS_CLIENTS,
MAX_UNIX_SOCKET_PATH_LENGTH,
assertValidUnixSocketPath,
formatUdsAddress,
parseUdsTarget,
sendUdsMessage,
Expand All @@ -34,11 +36,23 @@ let previousConfigDir: string | undefined
let tempConfigDir = ''

function socketPath(label: string): string {
const suffix = `${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}-${label}`
const suffix = `${process.pid}-${Math.random().toString(16).slice(2)}-${label}`
if (process.platform === 'win32') {
return `\\\\.\\pipe\\claude-code-test-${suffix}`
}
return join(tmpdir(), 'claude-code-test', `${suffix}.sock`)
const base =
process.platform === 'darwin'
? '/tmp/claude-uds-test'
: join(tmpdir(), 'cc-uds-test')
return join(base, `${suffix}.sock`)
}

function shortTestDir(prefix: string): string {
const id = `${process.pid}-${Math.random().toString(16).slice(2)}`
if (process.platform === 'darwin') {
return join('/tmp', `${prefix}-${id}`)
}
return join(tmpdir(), `${prefix}-${id}`)
}

function sleep(ms: number): Promise<void> {
Expand Down Expand Up @@ -499,6 +513,27 @@ describe('UDS inbox retention', () => {
expect(getDefaultUdsSocketPath()).not.toBe(firstPath)
})

test('default socket path stays within AF_UNIX length limit', () => {
const path = getDefaultUdsSocketPath()
if (process.platform === 'win32') return
expect(Buffer.byteLength(path, 'utf8')).toBeLessThanOrEqual(
MAX_UNIX_SOCKET_PATH_LENGTH,
)
expect(() => assertValidUnixSocketPath(path)).not.toThrow()
})

test('rejects socket paths longer than AF_UNIX limit', () => {
if (process.platform === 'win32') return
const longPath = `/tmp/${'x'.repeat(MAX_UNIX_SOCKET_PATH_LENGTH)}.sock`
expect(() => assertValidUnixSocketPath(longPath)).toThrow(/max 104/)
})

test('default socket path can bind on Node.js', async () => {
const path = getDefaultUdsSocketPath()
await startUdsMessaging(path, { isExplicit: true })
await stopUdsMessaging()
})

test('rejects oversized receiver responses before retaining them', async () => {
const path = socketPath('oversized-response')
if (process.platform !== 'win32') {
Expand Down Expand Up @@ -688,10 +723,7 @@ describe('UDS inbox retention', () => {
})

test('fails closed when an explicit socket parent is not private', async () => {
const parent = join(
tmpdir(),
`uds-socket-parent-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`,
)
const parent = shortTestDir('uds-sp')
await mkdir(parent, { recursive: true, mode: 0o755 })
await chmod(parent, 0o755)

Expand All @@ -707,10 +739,7 @@ describe('UDS inbox retention', () => {
})

test('fails closed when an explicit socket parent is a file', async () => {
const parentFile = join(
tmpdir(),
`uds-socket-parent-file-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`,
)
const parentFile = shortTestDir('uds-spf')
await writeFile(parentFile, 'not a directory', 'utf-8')

try {
Expand Down
25 changes: 21 additions & 4 deletions src/utils/udsMessaging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,31 +85,46 @@ export const MAX_UDS_CLIENTS = 128
export const UDS_AUTH_TIMEOUT_MS = 2_000
export const UDS_IDLE_TIMEOUT_MS = 30_000

/** macOS/BSD AF_UNIX `sun_path` limit (bytes, excluding NUL). */
export const MAX_UNIX_SOCKET_PATH_LENGTH = 104

// ---------------------------------------------------------------------------
// Public API — socket path helpers
// ---------------------------------------------------------------------------

export function assertValidUnixSocketPath(path: string): void {
if (process.platform === 'win32') return
const byteLength = Buffer.byteLength(path, 'utf8')
if (byteLength > MAX_UNIX_SOCKET_PATH_LENGTH) {
throw new Error(
`[udsMessaging] socket path is ${byteLength} bytes (max ${MAX_UNIX_SOCKET_PATH_LENGTH}): ${path}`,
)
}
}

/**
* Default socket path based on PID, placed in a tmpdir subdirectory so it
* survives across config-home changes and avoids polluting ~/.claude.
* Default socket path based on PID. Uses a flat file under a short temp
* directory so the path stays within the AF_UNIX limit on macOS.
*
* On Windows, Node.js requires named pipe paths in the `\\.\pipe\` namespace;
* file-system paths like `C:\...\Temp\x.sock` cause EACCES. Bun handles both
* transparently, but we use the pipe format on Windows for Node.js compat.
*/
export function getDefaultUdsSocketPath(): string {
if (defaultSocketPath) return defaultSocketPath
const nonce = randomBytes(16).toString('hex')
const nonce = randomBytes(8).toString('hex')
if (process.platform === 'win32') {
defaultSocketPath = `\\\\.\\pipe\\claude-code-${process.pid}-${nonce}`
return defaultSocketPath
}

defaultSocketPath = join(
tmpdir(),
'claude-code-socks',
'cc-socks',
`${process.pid}-${nonce}`,
'messaging.sock',
)
assertValidUnixSocketPath(defaultSocketPath)
return defaultSocketPath
}

Expand Down Expand Up @@ -416,6 +431,8 @@ export async function startUdsMessaging(
return
}

assertValidUnixSocketPath(path)

// Ensure parent directory exists (skip on Windows — pipe paths aren't files)
if (process.platform !== 'win32') {
await ensureSocketParent(path)
Expand Down