Skip to content

Commit 177448b

Browse files
fix(cli): forward codex passthrough args
1 parent 3f161de commit 177448b

9 files changed

Lines changed: 167 additions & 7 deletions

File tree

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ Step 3: Start using `happy` instead of `claude` or `codex`
5050
happy claude
5151
# or
5252
happy codex
53+
54+
# Forward Codex-only CLI flags after --
55+
happy codex -- --dangerously-bypass-approvals-and-sandbox
5356
```
5457

5558
## How does it work?

packages/happy-cli/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,14 @@ happy acp opencode
4141
happy acp -- custom-agent --flag
4242
```
4343

44+
Codex-only CLI flags can be forwarded after `--`:
45+
46+
```bash
47+
happy codex -- --dangerously-bypass-approvals-and-sandbox
48+
```
49+
50+
Arguments after `--` are passed to the Codex CLI before Happy starts Codex's app-server. They apply only to `happy codex`; other agents keep their existing argument handling.
51+
4452
## Daemon
4553

4654
The daemon is a background service that stays running on your machine. It lets you spawn and manage coding sessions remotely — from your phone or the web app — without needing an open terminal.

packages/happy-cli/src/codex/cliArgs.test.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expect, it } from 'vitest';
22

3-
import { extractCodexResumeFlag } from './cliArgs';
3+
import { extractCodexPassthroughArgs, extractCodexResumeFlag } from './cliArgs';
44

55
describe('extractCodexResumeFlag', () => {
66
it('returns null and preserves args when resume flag is absent', () => {
@@ -30,3 +30,39 @@ describe('extractCodexResumeFlag', () => {
3030
);
3131
});
3232
});
33+
34+
describe('extractCodexPassthroughArgs', () => {
35+
it('preserves all args as Happy args when delimiter is absent', () => {
36+
const parsed = extractCodexPassthroughArgs(['--resume', 'thread-123', '--model', 'gpt-5.5']);
37+
38+
expect(parsed).toEqual({
39+
happyArgs: ['--resume', 'thread-123', '--model', 'gpt-5.5'],
40+
codexArgs: [],
41+
});
42+
});
43+
44+
it('splits Codex args after the first delimiter', () => {
45+
const parsed = extractCodexPassthroughArgs([
46+
'--started-by',
47+
'terminal',
48+
'--',
49+
'--dangerously-bypass-approvals-and-sandbox',
50+
'--config',
51+
'model="gpt-5.5"',
52+
]);
53+
54+
expect(parsed).toEqual({
55+
happyArgs: ['--started-by', 'terminal'],
56+
codexArgs: ['--dangerously-bypass-approvals-and-sandbox', '--config', 'model="gpt-5.5"'],
57+
});
58+
});
59+
60+
it('keeps later delimiters in the Codex args', () => {
61+
const parsed = extractCodexPassthroughArgs(['--', '--config', 'foo=bar', '--', 'value']);
62+
63+
expect(parsed).toEqual({
64+
happyArgs: [],
65+
codexArgs: ['--config', 'foo=bar', '--', 'value'],
66+
});
67+
});
68+
});

packages/happy-cli/src/codex/cliArgs.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,3 +42,18 @@ export function extractCodexResumeFlag(args: string[]): { resumeThreadId: string
4242
args: remainingArgs,
4343
};
4444
}
45+
46+
export function extractCodexPassthroughArgs(args: string[]): { happyArgs: string[]; codexArgs: string[] } {
47+
const delimiterIndex = args.indexOf('--');
48+
if (delimiterIndex === -1) {
49+
return {
50+
happyArgs: args,
51+
codexArgs: [],
52+
};
53+
}
54+
55+
return {
56+
happyArgs: args.slice(0, delimiterIndex),
57+
codexArgs: args.slice(delimiterIndex + 1),
58+
};
59+
}

packages/happy-cli/src/codex/codexAppServerClient.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,60 @@ describe('CodexAppServerClient sandbox integration', () => {
162162
await client.disconnect();
163163
});
164164

165+
it('forwards Codex CLI args before the app-server subcommand', async () => {
166+
const { CodexAppServerClient } = await import('./codexAppServerClient');
167+
const client = new CodexAppServerClient(undefined, [
168+
'--dangerously-bypass-approvals-and-sandbox',
169+
'--config',
170+
'model="gpt-5.5"',
171+
]);
172+
173+
await client.connect();
174+
175+
expect(mockWrapForMcpTransport).not.toHaveBeenCalled();
176+
expect(mockSpawn).toHaveBeenCalledWith(
177+
'codex',
178+
[
179+
'--dangerously-bypass-approvals-and-sandbox',
180+
'--config',
181+
'model="gpt-5.5"',
182+
'app-server',
183+
'--listen',
184+
'stdio://',
185+
],
186+
expect.objectContaining({
187+
env: expect.objectContaining({
188+
RUST_LOG: expect.stringContaining('codex_core::rollout::list=off'),
189+
}),
190+
}),
191+
);
192+
193+
await client.disconnect();
194+
});
195+
196+
it('passes forwarded Codex CLI args into the sandbox wrapper', async () => {
197+
const { CodexAppServerClient } = await import('./codexAppServerClient');
198+
const client = new CodexAppServerClient(sandboxConfig, [
199+
'--dangerously-bypass-approvals-and-sandbox',
200+
]);
201+
202+
await client.connect();
203+
204+
expect(mockWrapForMcpTransport).toHaveBeenCalledWith('codex', [
205+
'--dangerously-bypass-approvals-and-sandbox',
206+
'app-server',
207+
'--listen',
208+
'stdio://',
209+
]);
210+
expect(mockSpawn).toHaveBeenCalledWith(
211+
'sh',
212+
['-c', 'wrapped codex app-server'],
213+
expect.anything(),
214+
);
215+
216+
await client.disconnect();
217+
});
218+
165219
it('falls back to non-sandbox transport when sandbox initialization fails', async () => {
166220
mockInitializeSandbox.mockRejectedValue(new Error('sandbox init failed'));
167221
const { CodexAppServerClient } = await import('./codexAppServerClient');

packages/happy-cli/src/codex/codexAppServerClient.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,7 @@ export class CodexAppServerClient {
218218
private processEpoch = 0;
219219
private connected = false;
220220
private sandboxConfig?: SandboxConfig;
221+
private codexCliArgs: string[];
221222
private sandboxCleanup: (() => Promise<void>) | null = null;
222223
public sandboxEnabled = false;
223224

@@ -257,8 +258,9 @@ export class CodexAppServerClient {
257258
private eventHandler: ((msg: EventMsg) => void) | null = null;
258259
private approvalHandler: ApprovalHandler | null = null;
259260

260-
constructor(sandboxConfig?: SandboxConfig) {
261+
constructor(sandboxConfig?: SandboxConfig, codexCliArgs: string[] = []) {
261262
this.sandboxConfig = sandboxConfig;
263+
this.codexCliArgs = [...codexCliArgs];
262264
}
263265

264266
get threadId(): string | null {
@@ -606,13 +608,13 @@ export class CodexAppServerClient {
606608
}
607609

608610
let command = 'codex';
609-
let args = ['app-server', '--listen', 'stdio://'];
611+
let args = [...this.codexCliArgs, 'app-server', '--listen', 'stdio://'];
610612
this.sandboxEnabled = false;
611613

612614
if (this.sandboxConfig?.enabled && process.platform !== 'win32') {
613615
try {
614616
this.sandboxCleanup = await initializeSandbox(this.sandboxConfig, process.cwd());
615-
const wrapped = await wrapForMcpTransport('codex', ['app-server', '--listen', 'stdio://']);
617+
const wrapped = await wrapForMcpTransport('codex', args);
616618
command = wrapped.command;
617619
args = wrapped.args;
618620
this.sandboxEnabled = true;

packages/happy-cli/src/codex/runCodex.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ export async function runCodex(opts: {
9595
permissionMode?: PermissionMode;
9696
model?: string;
9797
effort?: ReasoningEffort;
98+
codexCliArgs?: string[];
9899
}): Promise<void> {
99100
// Early check: ensure Codex CLI is installed before proceeding
100101
try {
@@ -592,7 +593,7 @@ export async function runCodex(opts: {
592593
// Start Context
593594
//
594595

595-
client = new CodexAppServerClient(sandboxConfig);
596+
client = new CodexAppServerClient(sandboxConfig, opts.codexCliArgs);
596597

597598
permissionHandler = new CodexPermissionHandler(session);
598599
// Drop any permission requests left in agent state from a previous CLI

packages/happy-cli/src/commands/codexCommand.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const mocks = vi.hoisted(() => ({
44
mockAuthAndSetupMachineIfNeeded: vi.fn(),
55
mockRunCodex: vi.fn(),
66
mockExtractCodexResumeFlag: vi.fn(),
7+
mockExtractCodexPassthroughArgs: vi.fn(),
78
mockExtractNoSandboxFlag: vi.fn(),
89
mockEnsureDaemonRunning: vi.fn(),
910
}))
@@ -18,6 +19,7 @@ vi.mock('@/codex/runCodex', () => ({
1819

1920
vi.mock('@/codex/cliArgs', () => ({
2021
extractCodexResumeFlag: mocks.mockExtractCodexResumeFlag,
22+
extractCodexPassthroughArgs: mocks.mockExtractCodexPassthroughArgs,
2123
}))
2224

2325
vi.mock('@/utils/sandboxFlags', () => ({
@@ -40,6 +42,10 @@ describe('handleCodexCommand', () => {
4042
noSandbox: false,
4143
args,
4244
}))
45+
mocks.mockExtractCodexPassthroughArgs.mockImplementation((args: string[]) => ({
46+
happyArgs: args,
47+
codexArgs: [],
48+
}))
4349
mocks.mockExtractCodexResumeFlag.mockImplementation((args: string[]) => ({
4450
resumeThreadId: null,
4551
args,
@@ -60,6 +66,7 @@ describe('handleCodexCommand', () => {
6066
permissionMode: undefined,
6167
model: undefined,
6268
effort: undefined,
69+
codexCliArgs: [],
6370
})
6471
expect(
6572
mocks.mockEnsureDaemonRunning.mock.invocationCallOrder[0],
@@ -86,6 +93,35 @@ describe('handleCodexCommand', () => {
8693
permissionMode: undefined,
8794
model: undefined,
8895
effort: undefined,
96+
codexCliArgs: [],
97+
})
98+
})
99+
100+
it('passes only delimiter-separated args through to the Codex CLI', async () => {
101+
mocks.mockExtractCodexPassthroughArgs.mockReturnValue({
102+
happyArgs: ['--started-by', 'terminal'],
103+
codexArgs: ['--dangerously-bypass-approvals-and-sandbox', '--config', 'model="gpt-5.5"'],
104+
})
105+
106+
await handleCodexCommand([
107+
'--started-by',
108+
'terminal',
109+
'--',
110+
'--dangerously-bypass-approvals-and-sandbox',
111+
'--config',
112+
'model="gpt-5.5"',
113+
])
114+
115+
expect(mocks.mockExtractNoSandboxFlag).toHaveBeenCalledWith(['--started-by', 'terminal'])
116+
expect(mocks.mockRunCodex).toHaveBeenCalledWith({
117+
credentials: { token: 'token' },
118+
startedBy: 'terminal',
119+
noSandbox: false,
120+
resumeThreadId: undefined,
121+
permissionMode: undefined,
122+
model: undefined,
123+
effort: undefined,
124+
codexCliArgs: ['--dangerously-bypass-approvals-and-sandbox', '--config', 'model="gpt-5.5"'],
89125
})
90126
})
91127

@@ -100,6 +136,7 @@ describe('handleCodexCommand', () => {
100136
permissionMode: 'yolo',
101137
model: undefined,
102138
effort: undefined,
139+
codexCliArgs: [],
103140
})
104141
})
105142

@@ -114,6 +151,7 @@ describe('handleCodexCommand', () => {
114151
permissionMode: 'yolo',
115152
model: undefined,
116153
effort: undefined,
154+
codexCliArgs: [],
117155
})
118156
})
119157

@@ -128,6 +166,7 @@ describe('handleCodexCommand', () => {
128166
permissionMode: undefined,
129167
model: 'gpt-5.4',
130168
effort: 'xhigh',
169+
codexCliArgs: [],
131170
})
132171
})
133172
})

packages/happy-cli/src/commands/codexCommand.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { authAndSetupMachineIfNeeded } from '@/ui/auth'
22
import { runCodex } from '@/codex/runCodex'
3-
import { extractCodexResumeFlag } from '@/codex/cliArgs'
3+
import { extractCodexPassthroughArgs, extractCodexResumeFlag } from '@/codex/cliArgs'
44
import { extractNoSandboxFlag } from '@/utils/sandboxFlags'
55
import { ensureDaemonRunning } from '@/daemon/ensureDaemonRunning'
66
import type { PermissionMode } from '@/api/types'
@@ -11,7 +11,8 @@ export async function handleCodexCommand(args: string[]): Promise<void> {
1111
let permissionMode: PermissionMode | undefined = undefined
1212
let model: string | undefined = undefined
1313
let effort: ReasoningEffort | undefined = undefined
14-
const sandboxArgs = extractNoSandboxFlag(args)
14+
const passthroughArgs = extractCodexPassthroughArgs(args)
15+
const sandboxArgs = extractNoSandboxFlag(passthroughArgs.happyArgs)
1516
const codexArgs = extractCodexResumeFlag(sandboxArgs.args)
1617

1718
for (let i = 0; i < codexArgs.args.length; i++) {
@@ -39,5 +40,6 @@ export async function handleCodexCommand(args: string[]): Promise<void> {
3940
permissionMode,
4041
model,
4142
effort,
43+
codexCliArgs: passthroughArgs.codexArgs,
4244
})
4345
}

0 commit comments

Comments
 (0)