{"slug": "i-stopped-choosing-between-claude-code-and-codex-i-put-both-in-one-chat-window", "title": "\"I Stopped Choosing Between Claude Code and Codex. I Put Both in One Chat Window\"", "summary": "A solution to the frustration of switching between coding agents Claude Code and Codex by integrating both into a single chat window. The author implemented a mode-switching system using simple commands like `/cligate` for assistant-mediated tasks and `/runtime` for direct runtime control, preventing the assistant from intercepting messages meant for the active coding session. This design prioritizes predictable behavior, ensuring that direct runtime messages are not hijacked unless the user explicitly requests assistant intervention.", "body_md": "Every \"Claude Code vs Codex\" comparison eventually runs into the same boring truth:\n\nI do not want to pick one forever.\n\nSome tasks feel better in Claude Code. Some feel better in Codex. Some days one account is rate-limited, one model is cheaper, or one runtime is already holding the context I need.\n\nThe annoying part is not choosing the better agent.\n\nThe annoying part is switching surfaces every time I change my mind.\n\n## The workflow I wanted\n\nI wanted one local chat window where I could do this:\n\n```\nUse Codex for this task.\nContinue that same runtime.\nSwitch to Claude Code for the next one.\nAsk the assistant to plan first.\nGo back to direct runtime mode.\n```\n\nThat sounds like a UI problem, but it is really a control problem.\n\nThere are two different things happening:\n\n- direct runtime work, where the next message should go straight to Claude Code or Codex\n- assistant-mediated work, where a supervisor decides whether to answer, ask a question, or delegate to a runtime\n\nIf those two modes are not explicit, the chat window turns into a trap. A short follow-up like:\n\n```\nmake it smaller\n```\n\ncan either mean:\n\n- continue the active Codex runtime\n- ask the product assistant\n- start a new Claude Code task\n- answer a pending approval\n\nGuessing wrong here is exactly how coding agents become frustrating.\n\n## So I made direct runtime the default\n\nIn [CliGate](https://github.com/codeking-ai/cligate), the chat UI conversation now defaults to direct runtime mode.\n\nThat was a deliberate choice.\n\nMost of the time, when I am using a coding agent, I do not want an assistant to intercept every message and \"think about what I meant.\" I want the current runtime to continue until I explicitly ask for something else.\n\nThere is a test that pins this behavior:\n\n```\ntest('ChatUiConversationStore defaults new chat-ui conversations to direct-runtime control mode', () => {\n  const conversation = conversationStore.findOrCreateBySessionId('chat-ui-default-direct-runtime-1');\n\n  assert.equal(conversation.metadata?.assistantCore?.mode, 'direct-runtime');\n  assert.equal(conversation.metadata?.assistantCore?.controlMode, 'direct-runtime');\n});\n```\n\nThat means a normal chat message does not automatically become \"assistant work.\" It stays on the runtime path.\n\n## The two commands that made the UI usable\n\nI ended up with a small mode switch instead of another complicated settings panel:\n\n```\n/cligate\n/runtime\n```\n\nThe mode parser is intentionally tiny:\n\n``` js\nconst cligateMatch = trimmed.match(/^\\/cligate(?:\\s+(.+))?$/is);\nif (cligateMatch) {\n  return {\n    command: 'cligate',\n    args: String(cligateMatch[1] || '').trim()\n  };\n}\n\nif (/^\\/runtime$/i.test(trimmed)) {\n  return {\n    command: 'runtime',\n    args: ''\n  };\n}\n```\n\nThe behavior is:\n\n-\n`/cligate`\n\nenters assistant mode -\n`/cligate <task>`\n\nruns one assistant-mediated task -\n`/runtime`\n\nexits assistant mode and returns to direct runtime routing\n\nThat one escape hatch matters.\n\nWhen I am done asking the assistant to plan or coordinate, I want the next message to go back to the active Claude Code or Codex session without ceremony.\n\n## The route now decides before touching the runtime\n\nThe chat route first gives the assistant mode service a chance to handle the message.\n\nIf assistant mode is not active and there is no `/cligate`\n\ncommand, it returns `null`\n\n, and the message goes down the normal runtime path:\n\n``` js\nconst assistantResult = await this.assistantModeService.maybeHandleMessage({\n  conversation,\n  text,\n  defaultRuntimeProvider,\n  cwd,\n  model,\n  executionMode: assistantExecutionMode,\n  onBackgroundResult\n});\n\nif (assistantResult) {\n  return {\n    ...assistantResult,\n    previousSessionId: conversation.activeRuntimeSessionId || null,\n    conversation: assistantResult.conversation || this.conversationStore.get(conversation.id)\n  };\n}\n```\n\nOnly after that does the service route directly to the runtime:\n\n``` js\nconst result = await this.messageService.routeUserMessage({\n  message: { text },\n  conversation,\n  defaultRuntimeProvider,\n  cwd,\n  model,\n  metadata: {\n    assistantMode: getAssistantControlMode(conversation),\n    source: {\n      kind: 'chat-ui',\n      sessionId: String(sessionId || ''),\n      conversationId: conversation.id\n    }\n  }\n});\n```\n\nThat separation is the whole point.\n\nThe assistant does not get to hijack direct runtime messages just because it exists.\n\n## Why this is better than a \"smart\" default\n\nI tried to make the assistant helpful.\n\nThen I realized \"helpful\" is dangerous in a coding workflow.\n\nIf a runtime is waiting for input, the least surprising thing is to send input to that runtime. If a task has a pending approval, the least surprising thing is to resolve that approval. If the user explicitly types `/cligate`\n\n, then the assistant can step in.\n\nThe result feels less magical, but much easier to trust.\n\nFor example:\n\n```\nfix the failing unit test\n```\n\ncan start a Codex runtime.\n\nThen:\n\n```\ntry the simpler patch\n```\n\ncontinues that runtime.\n\nThen:\n\n```\n/cligate compare this failure with the last run before continuing\n```\n\nlets the assistant reason over the situation.\n\nThen:\n\n```\n/runtime\n```\n\nputs the conversation back on the direct runtime path.\n\nThat is the loop I wanted.\n\n## Background runs needed their own guardrail\n\nThe other bug showed up after I made assistant runs asynchronous in the chat UI.\n\nIf an assistant-mediated task starts a runtime and returns later, the UI needs to persist the background result. But it must not append stale output from an older assistant run after the user has already started a newer one.\n\nSo the route records the pending assistant run ID:\n\n```\nif (result?.type === 'assistant_run_accepted' && result?.assistantRun?.id) {\n  chatUiConversationStore.patch(conversation.id, {\n    metadata: {\n      ...(conversation.metadata || {}),\n      uiChatPendingAssistantRunId: String(result.assistantRun.id || '').trim()\n    }\n  });\n}\n```\n\nAnd the background callback refuses stale results:\n\n```\nif (getPendingUiAssistantRunId(conversation) !== backgroundRunId) {\n  return;\n}\n```\n\nThat is not glamorous, but it prevents a very real UI bug:\n\n- start an assistant task\n- start another task before the first one finishes\n- watch the old answer appear under the new task\n\nNo thanks.\n\n## The model override bug was another small footgun\n\nThere was one more detail that mattered for a mixed Claude Code / Codex chat surface.\n\nThe normal chat UI has a local model selector. Runtime routing has its own provider semantics. If I let the local chat model override leak into runtime routing, I could accidentally send something like `gpt-5.4`\n\ninto a Claude Code runtime path where that was not the user's intent.\n\nSo for local chat-ui runtime messages, the route deliberately ignores the UI chat model override:\n\n``` js\nconst runtimeModelOverride = isExternalConversation ? String(model || '') : '';\n```\n\nThere is a test for that too:\n\n```\nassert.equal(captured[0].model, '');\nassert.equal(captured[0].defaultRuntimeProvider, 'claude-code');\n```\n\nThat tiny rule saved the UI from pretending that \"selected chat model\" and \"runtime provider\" are the same concept.\n\nThey are not.\n\n## What the setup looks like\n\nStart CliGate:\n\n```\nnpx cligate@latest start\n```\n\nOpen the dashboard:\n\n```\nhttp://localhost:8081\n```\n\nThen use the Chat page as the control surface:\n\n- choose Codex or Claude Code as the runtime provider\n- send a normal task to start direct runtime work\n- keep sending follow-ups to continue that runtime\n- use\n`/cligate`\n\nwhen you want assistant-mediated planning or delegation - use\n`/runtime`\n\nto return to the direct runtime path\n\nThat is the workflow I wanted from the beginning.\n\nNot \"which terminal agent wins?\"\n\nMore like:\n\n\"Can I keep both available without rebuilding my workflow around either one?\"\n\n## The lesson\n\nThe current wave of AI coding tools makes comparisons tempting.\n\nClaude Code vs Codex. Codex vs Gemini CLI. Terminal agent vs IDE agent.\n\nThose comparisons are useful, but they miss the day-to-day problem:\n\ndevelopers do not just choose tools. They move between them.\n\nFor me, the useful abstraction was not a smarter chatbot.\n\nIt was a chat control surface with explicit ownership:\n\n- direct runtime by default\n- assistant mode only when requested\n- sticky runtime continuation\n- stale background result protection\n- no accidental model override leaking into runtime work\n\nThat made Claude Code and Codex feel less like competing terminals and more like two workers behind the same local desk.\n\nIf you want to inspect the implementation, the project is here:\n\nI'm curious how other people are handling this. Are you choosing one coding agent, or are you building a workflow that lets several of them coexist?", "url": "https://wpnews.pro/news/i-stopped-choosing-between-claude-code-and-codex-i-put-both-in-one-chat-window", "canonical_source": "https://dev.to/codekingai/i-stopped-choosing-between-claude-code-and-codex-i-put-both-in-one-chat-window-2jpp", "published_at": "2026-05-20 06:19:34+00:00", "updated_at": "2026-05-20 06:32:35.563997+00:00", "lang": "en", "topics": ["developer-tools", "large-language-models", "artificial-intelligence"], "entities": ["Claude Code", "Codex", "CliGate"], "alternates": {"html": "https://wpnews.pro/news/i-stopped-choosing-between-claude-code-and-codex-i-put-both-in-one-chat-window", "markdown": "https://wpnews.pro/news/i-stopped-choosing-between-claude-code-and-codex-i-put-both-in-one-chat-window.md", "text": "https://wpnews.pro/news/i-stopped-choosing-between-claude-code-and-codex-i-put-both-in-one-chat-window.txt", "jsonld": "https://wpnews.pro/news/i-stopped-choosing-between-claude-code-and-codex-i-put-both-in-one-chat-window.jsonld"}}