@@ -40,6 +40,7 @@ const (
4040 EditorTerminal = "terminal"
4141 EditorTmux = "tmux"
4242 EditorClaude = "claude"
43+ EditorCodex = "codex"
4344)
4445
4546var (
@@ -52,6 +53,7 @@ Supported editors:
5253 terminal - Opens a new terminal window with SSH
5354 tmux - Opens a new terminal window with SSH + tmux session
5455 claude - Claude Code in a tmux session (auto-installs, auto-authenticates)
56+ codex - Codex CLI in a tmux session (auto-installs, auto-authenticates)
5557
5658Terminal support by platform:
5759 macOS: Terminal.app
@@ -105,7 +107,11 @@ You must have the editor installed in your path.`
105107
106108 # Pass flags through to Claude Code (use -- to separate brev flags from claude flags)
107109 brev open my-instance claude -- --model opus --allowedTools computer
108- brev open my-instance claude -- -p "fix the tests"`
110+ brev open my-instance claude -- -p "fix the tests"
111+
112+ # Open Codex CLI on a remote instance (installs if needed, auto-authenticates with OPENAI_API_KEY)
113+ brev open my-instance codex
114+ brev open my-instance codex -- --model o3`
109115)
110116
111117type OpenStore interface {
@@ -151,7 +157,7 @@ func NewCmdOpen(t *terminal.Terminal, store OpenStore, noLoginStartStore OpenSto
151157
152158 // Validate editor flag if provided
153159 if editor != "" && ! isEditorType (editor ) {
154- return breverrors .NewValidationError (fmt .Sprintf ("invalid editor: %s. Must be 'code', 'cursor', 'windsurf', 'terminal', 'tmux', or 'claude '" , editor ))
160+ return breverrors .NewValidationError (fmt .Sprintf ("invalid editor: %s. Must be 'code', 'cursor', 'windsurf', 'terminal', 'tmux', 'claude', or 'codex '" , editor ))
155161 }
156162
157163 // Get instance names and editor type from args or stdin
@@ -194,15 +200,15 @@ func NewCmdOpen(t *terminal.Terminal, store OpenStore, noLoginStartStore OpenSto
194200 cmd .Flags ().BoolVarP (& host , "host" , "" , false , "ssh into the host machine instead of the container" )
195201 cmd .Flags ().BoolVarP (& waitForSetupToFinish , "wait" , "w" , false , "wait for setup to finish" )
196202 cmd .Flags ().StringVarP (& directory , "dir" , "d" , "" , "directory to open" )
197- cmd .Flags ().StringVar (& setDefault , "set-default" , "" , "set default editor (code, cursor, windsurf, terminal, tmux, or claude )" )
198- cmd .Flags ().StringVarP (& editor , "editor" , "e" , "" , "editor to use (code, cursor, windsurf, terminal, tmux, or claude )" )
203+ cmd .Flags ().StringVar (& setDefault , "set-default" , "" , "set default editor (code, cursor, windsurf, terminal, tmux, claude, or codex )" )
204+ cmd .Flags ().StringVarP (& editor , "editor" , "e" , "" , "editor to use (code, cursor, windsurf, terminal, tmux, claude, or codex )" )
199205
200206 return cmd
201207}
202208
203209// isEditorType checks if a string is a valid editor type
204210func isEditorType (s string ) bool {
205- return s == EditorVSCode || s == EditorCursor || s == EditorWindsurf || s == EditorTerminal || s == EditorTmux || s == EditorClaude
211+ return s == EditorVSCode || s == EditorCursor || s == EditorWindsurf || s == EditorTerminal || s == EditorTmux || s == EditorClaude || s == EditorCodex
206212}
207213
208214// isPiped returns true if stdout is piped to another command
@@ -277,7 +283,7 @@ func getInstanceNamesAndEditor(args []string, editorFlag string) ([]string, stri
277283
278284func handleSetDefault (t * terminal.Terminal , editorType string ) error {
279285 if ! isEditorType (editorType ) {
280- return fmt .Errorf ("invalid editor type: %s. Must be 'code', 'cursor', 'windsurf', 'terminal', 'tmux', or 'claude '" , editorType )
286+ return fmt .Errorf ("invalid editor type: %s. Must be 'code', 'cursor', 'windsurf', 'terminal', 'tmux', 'claude', or 'codex '" , editorType )
281287 }
282288
283289 homeDir , err := os .UserHomeDir ()
@@ -382,6 +388,9 @@ func runOpenCommand(t *terminal.Terminal, tstore OpenStore, wsIDOrName string, s
382388 if strings .Contains (err .Error (), "failed to install Claude Code" ) {
383389 return breverrors .WrapAndTrace (err )
384390 }
391+ if strings .Contains (err .Error (), "failed to install Codex" ) {
392+ return breverrors .WrapAndTrace (err )
393+ }
385394 return breverrors .WrapAndTrace (err )
386395 }
387396 // Call analytics for open
@@ -555,6 +564,8 @@ func getEditorName(editorType string) string {
555564 return "tmux"
556565 case EditorClaude :
557566 return "Claude Code"
567+ case EditorCodex :
568+ return "Codex"
558569 default :
559570 return "VSCode"
560571 }
@@ -589,6 +600,8 @@ func openEditorByType(t *terminal.Terminal, editorType string, sshAlias string,
589600 return openTerminalWithTmux (sshAlias , path , tstore )
590601 case EditorClaude :
591602 return openClaude (t , sshAlias , path , editorArgs )
603+ case EditorCodex :
604+ return openCodex (t , sshAlias , path , editorArgs )
592605 default :
593606 tryToInstallExtensions (t , extensions )
594607 return openVsCode (sshAlias , path , tstore )
@@ -962,3 +975,102 @@ func ensureClaudeInstalled(t *terminal.Terminal, sshAlias string) error {
962975 t .Vprintf ("%s" , t .Green ("Claude Code installed successfully\n " ))
963976 return nil
964977}
978+
979+ func openCodex (t * terminal.Terminal , sshAlias string , path string , codexArgs []string ) error {
980+ // Ensure tmux is available on remote
981+ err := ensureTmuxInstalled (sshAlias )
982+ if err != nil {
983+ return breverrors .WrapAndTrace (fmt .Errorf ("tmux: command not found" ))
984+ }
985+
986+ // Install Codex remotely if not present
987+ err = ensureCodexInstalled (t , sshAlias )
988+ if err != nil {
989+ return breverrors .WrapAndTrace (err )
990+ }
991+
992+ // Auto-authenticate: only forward a key if the remote is not already logged in
993+ apiKey := resolveCodexAPIKey (t , sshAlias )
994+
995+ sessionName := "codex"
996+
997+ var envExport string
998+ if apiKey != "" {
999+ envExport = fmt .Sprintf ("export OPENAI_API_KEY=%s; " , shellescape .Quote (apiKey ))
1000+ }
1001+
1002+ // Build the codex command with any extra flags
1003+ codexCmd := "codex"
1004+ if len (codexArgs ) > 0 {
1005+ codexCmd = "codex " + strings .Join (codexArgs , " " )
1006+ }
1007+
1008+ // Prepend installer paths, set env if needed, then attach-or-create tmux session
1009+ remoteScript := fmt .Sprintf (
1010+ `export PATH="$HOME/.local/bin:$HOME/.npm-global/bin:$PATH"; %stmux has-session -t %s 2>/dev/null && tmux attach-session -t %s || (cd %s && tmux new-session -s %s %s)` ,
1011+ envExport , sessionName , sessionName , shellescape .Quote (path ), sessionName , shellescape .Quote (codexCmd ),
1012+ )
1013+
1014+ // Run SSH inline in the current terminal (interactive, with TTY)
1015+ sshCmd := exec .Command ("ssh" , "-t" , sshAlias , remoteScript ) // #nosec G204
1016+ sshCmd .Stdin = os .Stdin
1017+ sshCmd .Stdout = os .Stdout
1018+ sshCmd .Stderr = os .Stderr
1019+
1020+ err = sshCmd .Run ()
1021+ if err != nil {
1022+ return breverrors .WrapAndTrace (err )
1023+ }
1024+ return nil
1025+ }
1026+
1027+ // resolveCodexAPIKey returns an API key to forward to the remote, or "" if
1028+ // the remote is already authenticated or no local key can be found.
1029+ func resolveCodexAPIKey (t * terminal.Terminal , sshAlias string ) string {
1030+ // Check if remote already has OPENAI_API_KEY set
1031+ if isRemoteCodexAuthenticated (sshAlias ) {
1032+ return ""
1033+ }
1034+
1035+ // Check local OPENAI_API_KEY env var
1036+ if key := os .Getenv ("OPENAI_API_KEY" ); key != "" {
1037+ t .Vprintf ("%s" , t .Green ("Forwarding OPENAI_API_KEY to remote instance\n " ))
1038+ return key
1039+ }
1040+
1041+ return ""
1042+ }
1043+
1044+ // isRemoteCodexAuthenticated checks whether the remote already has
1045+ // OPENAI_API_KEY set in the shell.
1046+ func isRemoteCodexAuthenticated (sshAlias string ) bool {
1047+ checkCmd := exec .Command (
1048+ "ssh" , sshAlias ,
1049+ `printenv OPENAI_API_KEY >/dev/null 2>&1` ,
1050+ ) // #nosec G204
1051+ return checkCmd .Run () == nil
1052+ }
1053+
1054+ func ensureCodexInstalled (t * terminal.Terminal , sshAlias string ) error {
1055+ checkCmd := fmt .Sprintf (
1056+ "ssh %s 'export PATH=\" $HOME/.local/bin:$HOME/.npm-global/bin:$PATH\" ; which codex >/dev/null 2>&1'" ,
1057+ sshAlias ,
1058+ )
1059+ checkExec := exec .Command ("bash" , "-c" , checkCmd ) // #nosec G204
1060+ err := checkExec .Run ()
1061+ if err == nil {
1062+ return nil // already installed
1063+ }
1064+
1065+ t .Vprintf ("Installing Codex CLI on remote instance...\n " )
1066+
1067+ installCmd := fmt .Sprintf ("ssh %s 'npm install -g @openai/codex 2>/dev/null || sudo npm install -g @openai/codex'" , sshAlias )
1068+ installExec := exec .Command ("bash" , "-c" , installCmd ) // #nosec G204
1069+ output , err := installExec .CombinedOutput ()
1070+ if err != nil {
1071+ return fmt .Errorf ("failed to install Codex: %s\n %s" , err , string (output ))
1072+ }
1073+
1074+ t .Vprintf ("%s" , t .Green ("Codex CLI installed successfully\n " ))
1075+ return nil
1076+ }
0 commit comments