Merge pull request #110 from aakhter/cod-31-tmux-session-reliability

fix: harden tmux launch cwd
This commit is contained in:
Ark0N
2026-06-08 19:02:37 +02:00
committed by GitHub
2 changed files with 133 additions and 10 deletions
+24 -10
View File
@@ -73,6 +73,9 @@ const GRACEFUL_SHUTDOWN_WAIT_MS = 100;
/** Default stats collection interval (2 seconds) */
const DEFAULT_STATS_INTERVAL_MS = 2000;
/** Stable cwd for tmux server/pane launch; actual session cwd is reached inside the pane. */
const TMUX_LAUNCH_CWD = '/tmp';
/** Claude Code native macOS recommendation for avoiding low nofile startup failures. */
export const CLAUDE_CODE_NOFILE_LIMIT = 2147483646;
@@ -679,8 +682,10 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
// (Production uses systemd which has a clean env, but dev/test may be nested.)
const cleanEnv = { ...process.env };
delete cleanEnv.TMUX;
execSync(`${this.tmux()} new-session -ds "${muxName}" -c "${workingDir}"`, {
cwd: workingDir,
// Start the tmux server from a stable local cwd so FUSE/rclone workspace
// blips do not poison tmux's long-lived getcwd state.
execSync(`${this.tmux()} new-session -ds "${muxName}" -c ${TMUX_LAUNCH_CWD}`, {
cwd: TMUX_LAUNCH_CWD,
timeout: EXEC_TIMEOUT_MS,
stdio: 'ignore',
env: cleanEnv,
@@ -706,11 +711,16 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
// so secret values stay off the bash command line. Must run before respawn-pane.
this.applyEnvOverrides(muxName, envOverrides);
// Replace the shell with the actual command (no echo in terminal)
execSync(`${this.tmux()} respawn-pane -k -t "${muxName}" bash -c ${JSON.stringify(fullCmd)}`, {
timeout: EXEC_TIMEOUT_MS,
stdio: 'ignore',
});
// Replace the shell with the actual command (no echo in terminal). Keep
// pane launch in /tmp, then cd inside bash against the current mount table.
const launchCmd = `cd ${JSON.stringify(workingDir)} && ${fullCmd}`;
execSync(
`${this.tmux()} respawn-pane -k -c ${TMUX_LAUNCH_CWD} -t "${muxName}" bash -c ${JSON.stringify(launchCmd)}`,
{
timeout: EXEC_TIMEOUT_MS,
stdio: 'ignore',
}
);
// Wait for tmux session to be queryable
await new Promise((resolve) => setTimeout(resolve, TMUX_CREATION_WAIT_MS));
@@ -890,9 +900,13 @@ export class TmuxManager extends EventEmitter implements TerminalMultiplexer {
// Re-apply user env overrides before respawn so the new shell inherits them.
this.applyEnvOverrides(muxName, envOverrides);
await execAsync(`${this.tmux()} respawn-pane -k -t "${muxName}" bash -c ${JSON.stringify(fullCmd)}`, {
timeout: EXEC_TIMEOUT_MS,
});
const launchCmd = `cd ${JSON.stringify(workingDir)} && ${fullCmd}`;
await execAsync(
`${this.tmux()} respawn-pane -k -c ${TMUX_LAUNCH_CWD} -t "${muxName}" bash -c ${JSON.stringify(launchCmd)}`,
{
timeout: EXEC_TIMEOUT_MS,
}
);
// Wait for the respawned process to start
await new Promise((resolve) => setTimeout(resolve, TMUX_CREATION_WAIT_MS));
const pid = this.getPanePid(muxName);
+109
View File
@@ -20,6 +20,17 @@ vi.mock('node:child_process', async () => {
const actual = await vi.importActual('node:child_process');
return {
...actual,
exec: vi.fn((_cmd: string, optionsOrCallback?: unknown, maybeCallback?: unknown) => {
const callback = typeof optionsOrCallback === 'function' ? optionsOrCallback : maybeCallback;
if (typeof callback === 'function') {
setImmediate(() => callback(null, '', ''));
}
return {
on: vi.fn(),
kill: vi.fn(),
pid: 12345,
};
}),
execSync: vi.fn(),
spawn: vi.fn(() => ({
unref: vi.fn(),
@@ -41,6 +52,15 @@ vi.mock('node:fs', async () => {
};
});
vi.mock('node:fs/promises', async () => {
const actual = await vi.importActual('node:fs/promises');
return {
...actual,
writeFile: vi.fn(() => Promise.resolve()),
rename: vi.fn(() => Promise.resolve()),
};
});
describe('TmuxManager (unit)', () => {
let manager: TmuxManager;
const mockedExecSync = vi.mocked(execSync);
@@ -369,6 +389,95 @@ describe('TmuxManager (unit)', () => {
// No error thrown
});
});
describe('tmux launch cwd hardening', () => {
async function importWithTmuxCommandsEnabled(): Promise<typeof TmuxManager> {
const originalVitest = process.env.VITEST;
vi.resetModules();
delete process.env.VITEST;
const module = await import('../src/tmux-manager.js');
if (originalVitest === undefined) {
delete process.env.VITEST;
} else {
process.env.VITEST = originalVitest;
}
return module.TmuxManager;
}
beforeEach(() => {
mockedExecSync.mockImplementation((cmd: string) => {
if (typeof cmd === 'string' && cmd.includes('which tmux')) {
return '/usr/bin/tmux\n';
}
if (typeof cmd === 'string' && cmd.includes('display-message') && cmd.includes('#{pane_pid}')) {
return '4242\n';
}
return '';
});
});
it('starts new tmux sessions from /tmp and cd-bounces into the requested workspace', async () => {
const NonTestTmuxManager = await importWithTmuxCommandsEnabled();
const nonTestManager = new NonTestTmuxManager();
try {
const session = await nonTestManager.createSession({
sessionId: 'abc12345-1234-5678-90ab-cdef12345678',
workingDir: '/mnt/gdrive/project with spaces',
mode: 'shell',
});
expect(session.workingDir).toBe('/mnt/gdrive/project with spaces');
expect(session.pid).toBe(4242);
const newSessionCall = mockedExecSync.mock.calls.find(
([cmd]) => typeof cmd === 'string' && cmd.includes(' new-session ')
);
expect(newSessionCall?.[0]).toBe(`tmux -L 'codeman' new-session -ds "codeman-abc12345" -c /tmp`);
expect(newSessionCall?.[1]).toEqual(expect.objectContaining({ cwd: '/tmp' }));
const respawnCall = mockedExecSync.mock.calls.find(
([cmd]) => typeof cmd === 'string' && cmd.includes(' respawn-pane ')
);
expect(respawnCall?.[0]).toContain(`tmux -L 'codeman' respawn-pane -k -c /tmp -t "codeman-abc12345"`);
expect(respawnCall?.[0]).toContain('cd \\"/mnt/gdrive/project with spaces\\" &&');
} finally {
nonTestManager.destroy();
}
});
it('respawns existing panes from /tmp and cd-bounces into the requested workspace', async () => {
const NonTestTmuxManager = await importWithTmuxCommandsEnabled();
const nonTestManager = new NonTestTmuxManager();
nonTestManager.registerSession({
sessionId: 'respawn1234',
muxName: 'codeman-abcd1234',
pid: 1000,
createdAt: Date.now(),
workingDir: '/tmp',
mode: 'shell',
attached: false,
});
try {
const pid = await nonTestManager.respawnPane({
sessionId: 'respawn1234',
workingDir: '/mnt/gdrive/project',
mode: 'shell',
});
expect(pid).toBe(4242);
const { exec: currentExec } = await import('node:child_process');
const respawnCall = vi
.mocked(currentExec)
.mock.calls.find(([cmd]) => typeof cmd === 'string' && cmd.includes(' respawn-pane '));
expect(respawnCall?.[0]).toContain(`tmux -L 'codeman' respawn-pane -k -c /tmp -t "codeman-abcd1234"`);
expect(respawnCall?.[0]).toContain('cd \\"/mnt/gdrive/project\\" &&');
} finally {
nonTestManager.destroy();
}
});
});
});
// ============================================================================