{"slug": "wrapping-claude-code-and-codex-in-electron-a-stop-bug", "title": "Wrapping Claude Code and Codex in Electron: A Stop Bug", "summary": "A solo developer building Refmade, an Electron desktop wrapper that drives the user's own installed Claude Code and Codex binaries via stream-json and JSON-RPC over stdio, found during end-to-end testing that pressing Stop did not terminate background processes spawned by an agent — a detached `rm keep.txt` scheduled with nohup still executed 35 seconds after the task was stopped, deleting a file in a test folder. The app routes permission prompts through `--permission-prompt-tool stdio` for Claude Code and `codex app-server` approval requests for Codex, translating them into plain-language cards, and returns a JSON-RPC -32601 error for any request it does not recognize rather than guessing.", "body_md": "I pressed Stop on a task. When I looked 35 seconds later, a file in my test folder was gone.\n\nThe file was `keep.txt`, the folder was throwaway, and I had told the agent to set the trap itself: `nohup sh -c 'sleep 20; rm keep.txt' > /dev/null 2>&1 &`. Stop was supposed to end everything the task had started. It ended Claude Code, and the `rm` ran anyway. Of the bugs my end-to-end runs found in the wrapper app, this is the one that deleted a file.\n\nThis is part 9 of a series about Refmade, a desktop app I'm building alone. Refmade lets people who don't live in a terminal hand work to the Claude Code or Codex they have already installed, and shows the agents as an office you can watch. It is an independent app, not affiliated with Anthropic or OpenAI. Claude Code and Codex agents wrote most of the code at my direction; I planned, decided and reviewed. The UI is Korean right now; that changes later in this series. This part covers the evening of September 27 through September 29, when the app went from a web dashboard to an Electron shell around two command-line tools.\n\nA wrapper that calls the model API itself needs a key, and then it is a different product with a different bill. Refmade starts the `claude` or `codex` binary you installed, in the folder you picked, under your login. The header of the Codex runner in the current source says it plainly: the user's own binary and config are used as-is, and the app never reads auth or tokens.\n\nBoth tools turned out to have a machine-readable mode with a channel for permission questions. That channel is the whole trick.\n\nThe runner starts one long-lived `claude -p` per task. Everything on stdin and stdout is one JSON object per line.\n\n```\nclaude -p --input-format stream-json --output-format stream-json \\\n  --verbose --include-partial-messages --replay-user-messages \\\n  --permission-prompt-tool stdio\n# stdin, first line:\n{\"type\":\"control_request\",\"request_id\":\"init_1\",\"request\":{\"subtype\":\"initialize\",\"hooks\":null}}\n# answers to the CLI's questions reuse the question's id:\n{\"type\":\"control_response\",\"response\":{\"subtype\":\"success\",\"request_id\":\"<id>\",\"response\":{...}}}\n```\n\n`--permission-prompt-tool stdio` is what replaces the terminal prompt. When Claude Code wants to run a tool that its settings don't already allow, it sends a `can_use_tool` request on stdout. The app shows a card, waits for a click, and sends back a control response. `--replay-user-messages` makes the CLI echo each message it takes up, which is how the app knows whose turn it is when a background job finishes on its own. I pass no permission mode. Your `settings.json` rules apply, and a tool you allowed there runs without a card.\n\nCodex goes through `codex app-server`, newline-delimited JSON-RPC on stdio. My runner was written against the schema of Codex CLI 0.156.0.\n\n``` php\n Refmade window\n      |  IPC\n Refmade main process ---- JSON lines ----> claude -p            (your install, your login)\n      |                 \\-- JSON-RPC  ----> codex app-server     (your install, your login)\n      '-- plain-words card <---- permission request\n```\n\n|  | Claude Code | Codex | \n|---|---|---|\n| Process | `claude -p` , stream-json | `codex app-server` | \n| Permission asks | `can_use_tool` control request | `item/commandExecution/requestApproval` ,`item/fileChange/requestApproval` ,`item/permissions/requestApproval` | \n| Questions to the person | `AskUserQuestion` through the same channel | `item/tool/requestUserInput` | \n\nA request the runner doesn't know gets a JSON-RPC error back, `-32601`, with the text \"Refmade does not handle\" and the method name. I would rather fail loudly on a request I don't understand than guess an answer.\n\nA raw `rm -rf` prompt is useless to a person who has never used a shell. `main/plain.mjs` turns a request into a title, one plain sentence, a place (inside or outside the folder you picked), and chips for what changes. It is over 5,000 lines now.\n\nFor shell commands it splits on `|`, `&&`, `;`, `||` and newlines, reads each part by its first word, skips prefixes like `sudo`, `env` and `nohup`, and reads `bash -c '...'` from the inside. The sentence is always the app's own reading of the command. The AI's description is shown separately as \"AI explanation\", so a soft description can never hide what the command does. A program it can't read with confidence gets the `maybe` chip, and \"don't ask again\" is not offered for it.\n\n*A Codex permission card from my Sept 29 test run, in a throwaway test folder (Korean UI). Title: \"May I create a new file?\" What: \"Creates the file c2.txt in the w-codex-outside folder.\" Where, in red: \"Outside the folder you picked (your user folder)\". Changes: \"A new file appears\". Buttons: \"Yes, go ahead\", \"No, don't\", \"No, do it differently\".*\n\nThe two runners never answer a permission request by themselves. \"Don't ask again\" only forwards the CLI's own suggestion, scoped to the session. Later versions add an auto mode that answers light multiple-choice questions for you. It is not a safety guarantee, and a later post covers where I got that wrong in my own copy.\n\nNone of this is a sandbox. It is a reading of requests, and the reading can be wrong.\n\nClaude Code 2.1.283 runs each Bash command in a detached shell of its own. When I stopped a task, the app ended the CLI and its process group, and that did not reach a process the command had pushed into the background. `nohup sh -c 'sleep 20; rm keep.txt' &` outlived the Stop button and deleted the file.\n\nThe fix is in `main/procs.mjs`, and it is a heuristic. Linux can read another process's environment, so every CLI session gets a `REFMADE_TASK` variable and leftovers are found by it. macOS `ps` will not show another process's environment. There the app looks for processes that were orphaned (parent pid 1), started while that task's CLI was running, and whose working folder is the task folder or inside it. It leaves apps and system programs alone.\n\nKnown limit, written in the file: a dev server that you or another program started in the same folder while the task ran, and left running, looks identical. On Windows the function returns an empty list. Stop ends only the CLI's own process tree there, and the public Windows build is older than the Mac one. After the fix, the same test left `keep.txt` in place 35 seconds after Stop.\n\n*The same test after the fix, Sept 28, 10:05 to 10:08 PM (Korean UI). My message asks for the `nohup` command and then a `sleep 40`. The row says \"Thought it through, ran 1 command on the computer, may have deleted 1 file\". The status lines read \"Task stopped\" and \"Work running in the background stopped too\".*\n\nStart a task right after launching the app, and the team did not appear in the mini office within 60 seconds. If I opened the office first, it appeared in about a second. The collector's first read of the session logs takes around 4 seconds, and a session folder created during that read was never picked up. After the fix, a first task showed up in 6 seconds.\n\nAn end-to-end run against the real app found it, because the run sent a task in the first seconds after launch.\n\nThree rounds of adversarial review by separate agents went over this code. Round 1 ended with 36 fixes and round 2 with 35 more. After round 3 the app tests passed 394 of 394, three runs in a row. Those numbers say the tests agree with the code. They say nothing about whether anyone finds the app easy to use, and I haven't run a usability test with a non-developer.\n\nThe app link comes in the last post of the series.\n\nIf you wrap an agent CLI, how do you decide that a stopped session is really stopped: process-group kill, an environment tag like mine, or something else?", "url": "https://wpnews.pro/news/wrapping-claude-code-and-codex-in-electron-a-stop-bug", "canonical_source": "https://dev.to/ji_ai/wrapping-claude-code-and-codex-in-electron-a-stop-bug-ple", "published_at": "2026-10-28 19:01:46+00:00", "updated_at": "2026-10-28 19:11:32.504451+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools", "ai-products"], "entities": ["Refmade", "Claude Code", "Codex", "Anthropic", "OpenAI", "Electron"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/wrapping-claude-code-and-codex-in-electron-a-stop-bug", "markdown": "https://wpnews.pro/news/wrapping-claude-code-and-codex-in-electron-a-stop-bug.md", "text": "https://wpnews.pro/news/wrapping-claude-code-and-codex-in-electron-a-stop-bug.txt", "jsonld": "https://wpnews.pro/news/wrapping-claude-code-and-codex-in-electron-a-stop-bug.jsonld"}}