cd /news/ai-agents/wrapping-claude-code-and-codex-in-el… · home › topics › ai-agents › article
[ARTICLE · art-159807] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=↓ negative

Wrapping Claude Code and Codex in Electron: A Stop Bug

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.

by read6 min views1 publishedOct 28, 2026

I pressed Stop on a task. When I looked 35 seconds later, a file in my test folder was gone.

The 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.

This 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.

A 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.

Both tools turned out to have a machine-readable mode with a channel for permission questions. That channel is the whole trick.

The runner starts one long-lived claude -p per task. Everything on stdin and stdout is one JSON object per line.

claude -p --input-format stream-json --output-format stream-json \
  --verbose --include-partial-messages --replay-user-messages \
  --permission-prompt-tool stdio
{"type":"control_request","request_id":"init_1","request":{"subtype":"initialize","hooks":null}}
{"type":"control_response","response":{"subtype":"success","request_id":"<id>","response":{...}}}

--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.

Codex goes through codex app-server, newline-delimited JSON-RPC on stdio. My runner was written against the schema of Codex CLI 0.156.0.

 Refmade window
      |  IPC
 Refmade main process ---- JSON lines ----> claude -p            (your install, your login)
      |                 \-- JSON-RPC  ----> codex app-server     (your install, your login)
      '-- plain-words card <---- permission request
Claude Code Codex
Process claude -p , stream-json codex app-server
Permission asks can_use_tool control request item/commandExecution/requestApproval ,item/fileChange/requestApproval ,item/permissions/requestApproval
Questions to the person AskUserQuestion through the same channel item/tool/requestUserInput

A 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.

A 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.

For 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.

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".

The 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.

None of this is a sandbox. It is a reading of requests, and the reading can be wrong.

Claude 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.

The 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.

Known 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.

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".

Start 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.

An end-to-end run against the real app found it, because the run sent a task in the first seconds after launch.

Three 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.

The app link comes in the last post of the series.

If 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?

── more in #ai-agents 4 stories · sorted by recency
── more on @refmade 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/wrapping-claude-code…] indexed:0 read:6min 2026-10-28 · —