[{"data":1,"prerenderedAt":1278},["ShallowReactive",2],{"article:running-coding-agents-in-disposable-docker-containers":3,"\u002Fblog\u002Frunning-coding-agents-in-disposable-docker-containers-surround":1267},{"id":4,"title":5,"archived":6,"author":7,"body":8,"date":1253,"description":1254,"extension":1255,"image":1256,"meta":1257,"minRead":325,"navigation":103,"path":1258,"published":103,"seo":1259,"sitemap":1260,"stem":1261,"tags":1262,"__hash__":1266},"blog\u002Fblog\u002Frunning-coding-agents-in-disposable-docker-containers.md","Running Coding Agents in Disposable Docker Containers",false,"Baljeet Singh",{"type":9,"value":10,"toc":1241},"minimark",[11,31,34,42,45,50,53,619,622,625,652,692,699,703,706,712,719,722,745,748,754,760,763,767,770,773,790,793,797,800,826,833,836,840,843,849,858,861,883,888,900,916,920,923,941,952,956,963,970,974,977,983,989,1072,1085,1103,1109,1149,1156,1160,1166,1172,1185,1203,1214,1221,1225,1228,1234,1237],[12,13,14,15,19,20,19,23,26,27,30],"p",{},"Give an agent ",[16,17,18],"code",{},"Read",", ",[16,21,22],{},"Edit",[16,24,25],{},"Write"," and ",[16,28,29],{},"Bash"," and it becomes genuinely useful. It also becomes something running shell commands on your machine, in a loop, at whatever pace the model decides.",[12,32,33],{},"Most agent demos run in your terminal, in your repo, with your credentials. That's fine for a demo. It is not fine for something you leave running while you make tea, or for a task you want three of at once.",[12,35,36,37,41],{},"So: ",[38,39,40],"strong",{},"one Docker container per task."," An orchestrator that spawns them, a hard budget cap inside each one, and a small HTTP server in every container so you can talk to a running agent instead of only watching it.",[12,43,44],{},"Here is the shape.",[46,47,49],"h2",{"id":48},"the-smallest-version","The Smallest Version",[12,51,52],{},"The Claude Agent SDK is an async iterator. This is a complete agent:",[54,55,60],"pre",{"className":56,"code":57,"language":58,"meta":59,"style":59},"language-js shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","import { query } from \"@anthropic-ai\u002Fclaude-agent-sdk\";\n\nfor await (const message of query({\n  prompt: \"Read \u002Fproject\u002Findex.js and add error handling to the routes.\",\n  options: {\n    tools: [\"Read\", \"Edit\", \"Write\", \"Glob\", \"Bash\"],\n    allowedTools: [\"Read\", \"Edit\", \"Write\", \"Glob\", \"Bash\"],\n    permissionMode: \"acceptEdits\",\n    model: \"claude-sonnet-4-6\",\n    maxTurns: 50,\n    maxBudgetUsd: 1.0,\n    cwd: \"\u002Fproject\",\n  },\n})) {\n  if (message.type === \"assistant\" && message.message?.content) {\n    for (const block of message.message.content) {\n      if (\"text\" in block) console.log(block.text);\n      if (\"name\" in block) console.log(`Tool: ${block.name}`);\n    }\n  } else if (message.type === \"result\") {\n    console.log(\"\\n--- Done ---\");\n  }\n}\n","js","",[16,61,62,98,105,136,156,167,223,275,292,309,323,336,353,359,370,418,448,492,542,548,581,607,613],{"__ignoreMap":59},[63,64,67,71,75,79,82,85,88,92,95],"span",{"class":65,"line":66},"line",1,[63,68,70],{"class":69},"s7zQu","import",[63,72,74],{"class":73},"sMK4o"," {",[63,76,78],{"class":77},"sTEyZ"," query",[63,80,81],{"class":73}," }",[63,83,84],{"class":69}," from",[63,86,87],{"class":73}," \"",[63,89,91],{"class":90},"sfazB","@anthropic-ai\u002Fclaude-agent-sdk",[63,93,94],{"class":73},"\"",[63,96,97],{"class":73},";\n",[63,99,101],{"class":65,"line":100},2,[63,102,104],{"emptyLinePlaceholder":103},true,"\n",[63,106,108,111,114,117,121,124,127,130,133],{"class":65,"line":107},3,[63,109,110],{"class":69},"for",[63,112,113],{"class":69}," await",[63,115,116],{"class":77}," (",[63,118,120],{"class":119},"spNyl","const",[63,122,123],{"class":77}," message ",[63,125,126],{"class":73},"of",[63,128,78],{"class":129},"s2Zo4",[63,131,132],{"class":77},"(",[63,134,135],{"class":73},"{\n",[63,137,139,143,146,148,151,153],{"class":65,"line":138},4,[63,140,142],{"class":141},"swJcz","  prompt",[63,144,145],{"class":73},":",[63,147,87],{"class":73},[63,149,150],{"class":90},"Read \u002Fproject\u002Findex.js and add error handling to the routes.",[63,152,94],{"class":73},[63,154,155],{"class":73},",\n",[63,157,159,162,164],{"class":65,"line":158},5,[63,160,161],{"class":141},"  options",[63,163,145],{"class":73},[63,165,166],{"class":73}," {\n",[63,168,170,173,175,178,180,182,184,187,189,191,193,195,197,199,201,203,205,208,210,212,214,216,218,221],{"class":65,"line":169},6,[63,171,172],{"class":141},"    tools",[63,174,145],{"class":73},[63,176,177],{"class":77}," [",[63,179,94],{"class":73},[63,181,18],{"class":90},[63,183,94],{"class":73},[63,185,186],{"class":73},",",[63,188,87],{"class":73},[63,190,22],{"class":90},[63,192,94],{"class":73},[63,194,186],{"class":73},[63,196,87],{"class":73},[63,198,25],{"class":90},[63,200,94],{"class":73},[63,202,186],{"class":73},[63,204,87],{"class":73},[63,206,207],{"class":90},"Glob",[63,209,94],{"class":73},[63,211,186],{"class":73},[63,213,87],{"class":73},[63,215,29],{"class":90},[63,217,94],{"class":73},[63,219,220],{"class":77},"]",[63,222,155],{"class":73},[63,224,226,229,231,233,235,237,239,241,243,245,247,249,251,253,255,257,259,261,263,265,267,269,271,273],{"class":65,"line":225},7,[63,227,228],{"class":141},"    allowedTools",[63,230,145],{"class":73},[63,232,177],{"class":77},[63,234,94],{"class":73},[63,236,18],{"class":90},[63,238,94],{"class":73},[63,240,186],{"class":73},[63,242,87],{"class":73},[63,244,22],{"class":90},[63,246,94],{"class":73},[63,248,186],{"class":73},[63,250,87],{"class":73},[63,252,25],{"class":90},[63,254,94],{"class":73},[63,256,186],{"class":73},[63,258,87],{"class":73},[63,260,207],{"class":90},[63,262,94],{"class":73},[63,264,186],{"class":73},[63,266,87],{"class":73},[63,268,29],{"class":90},[63,270,94],{"class":73},[63,272,220],{"class":77},[63,274,155],{"class":73},[63,276,278,281,283,285,288,290],{"class":65,"line":277},8,[63,279,280],{"class":141},"    permissionMode",[63,282,145],{"class":73},[63,284,87],{"class":73},[63,286,287],{"class":90},"acceptEdits",[63,289,94],{"class":73},[63,291,155],{"class":73},[63,293,295,298,300,302,305,307],{"class":65,"line":294},9,[63,296,297],{"class":141},"    model",[63,299,145],{"class":73},[63,301,87],{"class":73},[63,303,304],{"class":90},"claude-sonnet-4-6",[63,306,94],{"class":73},[63,308,155],{"class":73},[63,310,312,315,317,321],{"class":65,"line":311},10,[63,313,314],{"class":141},"    maxTurns",[63,316,145],{"class":73},[63,318,320],{"class":319},"sbssI"," 50",[63,322,155],{"class":73},[63,324,326,329,331,334],{"class":65,"line":325},11,[63,327,328],{"class":141},"    maxBudgetUsd",[63,330,145],{"class":73},[63,332,333],{"class":319}," 1.0",[63,335,155],{"class":73},[63,337,339,342,344,346,349,351],{"class":65,"line":338},12,[63,340,341],{"class":141},"    cwd",[63,343,145],{"class":73},[63,345,87],{"class":73},[63,347,348],{"class":90},"\u002Fproject",[63,350,94],{"class":73},[63,352,155],{"class":73},[63,354,356],{"class":65,"line":355},13,[63,357,358],{"class":73},"  },\n",[63,360,362,365,368],{"class":65,"line":361},14,[63,363,364],{"class":73},"}",[63,366,367],{"class":77},")) ",[63,369,135],{"class":73},[63,371,373,376,378,381,384,387,390,392,395,397,400,403,405,407,410,413,416],{"class":65,"line":372},15,[63,374,375],{"class":69},"  if",[63,377,116],{"class":141},[63,379,380],{"class":77},"message",[63,382,383],{"class":73},".",[63,385,386],{"class":77},"type",[63,388,389],{"class":73}," ===",[63,391,87],{"class":73},[63,393,394],{"class":90},"assistant",[63,396,94],{"class":73},[63,398,399],{"class":73}," &&",[63,401,402],{"class":77}," message",[63,404,383],{"class":73},[63,406,380],{"class":77},[63,408,409],{"class":73},"?.",[63,411,412],{"class":77},"content",[63,414,415],{"class":141},") ",[63,417,135],{"class":73},[63,419,421,424,426,428,431,434,436,438,440,442,444,446],{"class":65,"line":420},16,[63,422,423],{"class":69},"    for",[63,425,116],{"class":141},[63,427,120],{"class":119},[63,429,430],{"class":77}," block",[63,432,433],{"class":73}," of",[63,435,402],{"class":77},[63,437,383],{"class":73},[63,439,380],{"class":77},[63,441,383],{"class":73},[63,443,412],{"class":77},[63,445,415],{"class":141},[63,447,135],{"class":73},[63,449,451,454,456,458,461,463,466,468,470,473,475,478,480,483,485,487,490],{"class":65,"line":450},17,[63,452,453],{"class":69},"      if",[63,455,116],{"class":141},[63,457,94],{"class":73},[63,459,460],{"class":90},"text",[63,462,94],{"class":73},[63,464,465],{"class":73}," in",[63,467,430],{"class":77},[63,469,415],{"class":141},[63,471,472],{"class":77},"console",[63,474,383],{"class":73},[63,476,477],{"class":129},"log",[63,479,132],{"class":141},[63,481,482],{"class":77},"block",[63,484,383],{"class":73},[63,486,460],{"class":77},[63,488,489],{"class":141},")",[63,491,97],{"class":73},[63,493,495,497,499,501,504,506,508,510,512,514,516,518,520,523,526,529,531,533,535,538,540],{"class":65,"line":494},18,[63,496,453],{"class":69},[63,498,116],{"class":141},[63,500,94],{"class":73},[63,502,503],{"class":90},"name",[63,505,94],{"class":73},[63,507,465],{"class":73},[63,509,430],{"class":77},[63,511,415],{"class":141},[63,513,472],{"class":77},[63,515,383],{"class":73},[63,517,477],{"class":129},[63,519,132],{"class":141},[63,521,522],{"class":73},"`",[63,524,525],{"class":90},"Tool: ",[63,527,528],{"class":73},"${",[63,530,482],{"class":77},[63,532,383],{"class":73},[63,534,503],{"class":77},[63,536,537],{"class":73},"}`",[63,539,489],{"class":141},[63,541,97],{"class":73},[63,543,545],{"class":65,"line":544},19,[63,546,547],{"class":73},"    }\n",[63,549,551,554,557,560,562,564,566,568,570,572,575,577,579],{"class":65,"line":550},20,[63,552,553],{"class":73},"  }",[63,555,556],{"class":69}," else",[63,558,559],{"class":69}," if",[63,561,116],{"class":141},[63,563,380],{"class":77},[63,565,383],{"class":73},[63,567,386],{"class":77},[63,569,389],{"class":73},[63,571,87],{"class":73},[63,573,574],{"class":90},"result",[63,576,94],{"class":73},[63,578,415],{"class":141},[63,580,135],{"class":73},[63,582,584,587,589,591,593,595,598,601,603,605],{"class":65,"line":583},21,[63,585,586],{"class":77},"    console",[63,588,383],{"class":73},[63,590,477],{"class":129},[63,592,132],{"class":141},[63,594,94],{"class":73},[63,596,597],{"class":77},"\\n",[63,599,600],{"class":90},"--- Done ---",[63,602,94],{"class":73},[63,604,489],{"class":141},[63,606,97],{"class":73},[63,608,610],{"class":65,"line":609},22,[63,611,612],{"class":73},"  }\n",[63,614,616],{"class":65,"line":615},23,[63,617,618],{"class":73},"}\n",[12,620,621],{},"That is the whole loop. The SDK handles tool execution — you don't write a dispatch switch, you declare which tools it may use and it runs them.",[12,623,624],{},"Two of those options are easy to get wrong, and I had both wrong to start with.",[12,626,627,637,638,640,641,645,646,648,649,651],{},[38,628,629,632,633,636],{},[16,630,631],{},"tools"," restricts; ",[16,634,635],{},"allowedTools"," only auto-approves."," They read like synonyms and they are not. ",[16,639,635],{}," is the list that runs without stopping to ask permission — leaving a tool ",[642,643,644],"em",{},"out"," of it does not take the tool away, it just means the agent gets prompted for it. The option that decides what exists at all is ",[16,647,631],{},". If you are trying to hand an agent a smaller surface, ",[16,650,635],{}," alone will not do it, and nothing will tell you.",[12,653,654,663,664,667,668,671,672,675,676,679,680,683,684,687,688,691],{},[38,655,656,26,659,662],{},[16,657,658],{},"maxTurns",[16,660,661],{},"maxBudgetUsd"," are where the caps actually live."," Setting them in the environment and reading them into a config object does nothing; they have to reach ",[16,665,666],{},"query()",". When either is hit the SDK stops the run and ends the stream with a result whose ",[16,669,670],{},"subtype"," is ",[16,673,674],{},"error_max_turns"," or ",[16,677,678],{},"error_max_budget_usd",". It does ",[38,681,682],{},"not"," throw — so if your loop only watches for exceptions, a run that burned through its budget looks exactly like one that finished, and you will believe a cap is holding when you have never once seen it fire. Check ",[16,685,686],{},"message.subtype",", and record ",[16,689,690],{},"message.total_cost_usd"," while you are there.",[12,693,694,695,698],{},"Note ",[16,696,697],{},"permissionMode: \"acceptEdits\"",". The agent edits files without asking. In your terminal that is a decision you should think hard about. Inside a throwaway container with one directory mounted, it's just how the thing works.",[46,700,702],{"id":701},"two-processes-not-one","Two Processes, Not One",[12,704,705],{},"The architecture is an orchestrator that is always up, and agent containers that are not.",[12,707,708,711],{},[38,709,710],{},"The orchestrator"," serves a web UI and manages containers:",[54,713,717],{"className":714,"code":716,"language":460},[715],"language-text","POST   \u002Ftasks              → docker run, one new agent container\nPOST   \u002Ftasks\u002F:id\u002Fmessage  → proxy a follow-up to that container\nGET    \u002Ftasks\u002F:id\u002Fevents   → poll the agent, push to the browser over SSE\nDELETE \u002Ftasks\u002F:id          → shut the agent down, stop and remove\n",[16,718,716],{"__ignoreMap":59},[12,720,721],{},"It needs the Docker socket to do that, which is the one genuinely sharp edge in the whole design:",[54,723,727],{"className":724,"code":725,"language":726,"meta":59,"style":59},"language-yaml shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","volumes:\n  - \u002Fvar\u002Frun\u002Fdocker.sock:\u002Fvar\u002Frun\u002Fdocker.sock\n","yaml",[16,728,729,737],{"__ignoreMap":59},[63,730,731,734],{"class":65,"line":66},[63,732,733],{"class":141},"volumes",[63,735,736],{"class":73},":\n",[63,738,739,742],{"class":65,"line":100},[63,740,741],{"class":73},"  -",[63,743,744],{"class":90}," \u002Fvar\u002Frun\u002Fdocker.sock:\u002Fvar\u002Frun\u002Fdocker.sock\n",[12,746,747],{},"Mounting the Docker socket into a container gives that container control of your Docker daemon. It's how the orchestrator creates siblings, and it is effectively root on the host. Fine for a local tool on your own machine. Not something to expose to a network without thinking about it properly.",[12,749,750,753],{},[38,751,752],{},"Each agent container"," runs the loop plus a tiny Express control server on port 8080:",[54,755,758],{"className":756,"code":757,"language":460},[715],"GET    \u002Fhealth     → is it up\nGET    \u002Fstatus     → events since cursor N\nPOST   \u002Fmessage    → queue a follow-up\nPOST   \u002Fpause      → stop after the current turn\nDELETE \u002Fshutdown   → exit the loop cleanly\n",[16,759,757],{"__ignoreMap":59},[12,761,762],{},"The control server and the agent loop share one in-process state module — status, an event list, a message queue. That's the entire coordination mechanism. No Redis, no database.",[46,764,766],{"id":765},"why-a-server-inside-the-container","Why a Server Inside the Container",[12,768,769],{},"This is the part that took me longest to appreciate.",[12,771,772],{},"A one-shot agent is easy: pass a prompt, wait, read the output. But agents get stuck, or go the wrong way, or finish and leave you wanting one more thing. Without a way in, your only options are watch or kill.",[12,774,775,776,779,780,782,783,786,787,789],{},"The queue changes that. A follow-up arrives on ",[16,777,778],{},"POST \u002Fmessage"," while ",[16,781,666],{}," is mid-run. It goes into ",[16,784,785],{},"messageQueue",". When the current query finishes, the loop drains the queue and starts another ",[16,788,666],{}," with the follow-up as the prompt. The container stays alive between turns.",[12,791,792],{},"So a task is a conversation with a process, not a single invocation. The container lives until you delete it.",[46,794,796],{"id":795},"the-caps-nobody-adds","The Caps Nobody Adds",[12,798,799],{},"Two environment variables that should be in every agent you write:",[54,801,803],{"className":724,"code":802,"language":726,"meta":59,"style":59},"environment:\n  - MAX_BUDGET_USD=${MAX_BUDGET_USD:-1.0}\n  - MAX_TURNS=${MAX_TURNS:-50}\n",[16,804,805,812,819],{"__ignoreMap":59},[63,806,807,810],{"class":65,"line":66},[63,808,809],{"class":141},"environment",[63,811,736],{"class":73},[63,813,814,816],{"class":65,"line":100},[63,815,741],{"class":73},[63,817,818],{"class":90}," MAX_BUDGET_USD=${MAX_BUDGET_USD:-1.0}\n",[63,820,821,823],{"class":65,"line":107},[63,822,741],{"class":73},[63,824,825],{"class":90}," MAX_TURNS=${MAX_TURNS:-50}\n",[12,827,828,829,832],{},"An agent loop is a ",[16,830,831],{},"while"," loop with an API bill attached. The failure mode is not that it breaks, it's that it doesn't — it keeps trying, politely, until you notice.",[12,834,835],{},"A dollar cap and a turn cap cost nothing to add and they are the difference between a bad afternoon and a bad invoice. Add them before you add anything else.",[46,837,839],{"id":838},"coding-mode-and-general-mode","Coding Mode and General Mode",[12,841,842],{},"The same container runs two kinds of agent, chosen at task creation. The difference is the system prompt and the tool set.",[12,844,845,848],{},[38,846,847],{},"Coding mode"," gets the filesystem and shell tools, plus git. It is pointed at a mounted project directory and expected to leave changes in it.",[12,850,851,854,855,857],{},[38,852,853],{},"General mode"," gets a narrower set. It answers questions, fetches things, produces text. It doesn't need ",[16,856,22],{}," and shouldn't have it.",[12,859,860],{},"Alongside the built-in tools, both get MCP tools for talking back:",[862,863,864,871,877],"ul",{},[865,866,867,870],"li",{},[16,868,869],{},"send_update"," — post progress to the event stream",[865,872,873,876],{},[16,874,875],{},"request_clarification"," — ask the user something and wait",[865,878,879,882],{},[16,880,881],{},"git_status"," — what have I actually changed",[12,884,885,887],{},[16,886,875],{}," is the interesting one, because it makes the agent a participant rather than a batch job. It writes a question to the event stream and blocks. The user answers through the same message queue a follow-up uses. The agent continues.",[12,889,890,891,894,895,897,898,383],{},"Splitting modes is not about capability, it's about ",[38,892,893],{},"the smallest tool set that can do the job."," An agent with ",[16,896,29],{}," will find a way to use ",[16,899,29],{},[12,901,902,903,905,906,19,908,26,910,912,913,915],{},"Which is exactly why the split has to happen in ",[16,904,631],{}," and not only in the prompt. Mine did not, for longer than I would like: both modes were handed the same hardcoded tool array and only the system prompt and the MCP tools varied, so \"general mode\" had ",[16,907,22],{},[16,909,25],{},[16,911,29],{}," the whole time. The mode was a suggestion. If you build this, branch the ",[16,914,631],{}," array on the mode and confirm it from the outside — ask general mode to write a file and watch it refuse, rather than trusting that it won't try.",[46,917,919],{"id":918},"hooks-for-output-discipline","Hooks, for Output Discipline",[12,921,922],{},"The SDK fires hooks around every tool call. Two are worth wiring up immediately:",[862,924,925,933],{},[865,926,927,932],{},[38,928,929],{},[16,930,931],{},"PreToolUse"," — log the call. This is your entire audit trail, and you'll want it the first time an agent does something surprising.",[865,934,935,940],{},[38,936,937],{},[16,938,939],{},"PostToolUse"," — truncate output over ~5,000 characters.",[12,942,943,944,947,948,951],{},"The truncation matters more than it sounds. One ",[16,945,946],{},"cat"," of a large file, or a ",[16,949,950],{},"grep"," across a monorepo, and the result goes straight into the context window. A few of those and you have burned the budget on tool output nobody reads. Truncating at the boundary is much easier than teaching the model to be careful.",[46,953,955],{"id":954},"streaming-it-to-a-ui","Streaming It to a UI",[12,957,958,959,962],{},"The orchestrator polls each agent's ",[16,960,961],{},"\u002Fstatus?since=N"," roughly once a second and pushes new events to the browser over Server-Sent Events.",[12,964,965,966,969],{},"Polling inside, SSE outside. It isn't elegant, and it's completely fine: the cursor makes the poll cheap and idempotent, SSE is one line in the browser, and the whole thing is debuggable with ",[16,967,968],{},"curl",". Reach for websockets when you have a reason.",[46,971,973],{"id":972},"what-here-is-claude-specific-and-what-isnt","What Here Is Claude-Specific, and What Isn't",[12,975,976],{},"Everything above is built on the Claude Agent SDK, and the obvious question is how much of it survives a change of SDK. I went and looked, because I assumed the answer was \"you'd have to write the tools yourself\" and that turned out to be wrong.",[12,978,979,982],{},[38,980,981],{},"The container half is not Anthropic-specific at all."," One container per task, the mounted project directory, the disposable blast radius, the control server, the message queue, the event cursor, the SSE fan-out — none of that knows what model is inside. That is the part worth stealing, and it is most of the design.",[12,984,985,988],{},[38,986,987],{},"Every major SDK ships coding tools."," This is the assumption I had backwards. It is not the case that Anthropic gives you a filesystem agent and everyone else leaves you to build one:",[990,991,992,1007],"table",{},[993,994,995],"thead",{},[996,997,998,1001,1004],"tr",{},[999,1000],"th",{},[999,1002,1003],{},"file + shell tools",[999,1005,1006],{},"custom tools",[1008,1009,1010,1037,1051],"tbody",{},[996,1011,1012,1016,1031],{},[1013,1014,1015],"td",{},"Claude Agent SDK",[1013,1017,1018,19,1020,19,1022,19,1024,19,1026,19,1029],{},[16,1019,18],{},[16,1021,22],{},[16,1023,25],{},[16,1025,207],{},[16,1027,1028],{},"Grep",[16,1030,29],{},[1013,1032,1033,1034],{},"an MCP server via ",[16,1035,1036],{},"createSdkMcpServer",[996,1038,1039,1042,1045],{},[1013,1040,1041],{},"OpenAI Agents SDK",[1013,1043,1044],{},"built-in shell, apply-patch and computer-use tools, plus sandbox tools bound to an isolated workspace",[1013,1046,1047,1050],{},[16,1048,1049],{},"tool()"," with a Zod schema",[996,1052,1053,1056,1066],{},[1013,1054,1055],{},"Google ADK",[1013,1057,1058,1061,1062,1065],{},[16,1059,1060],{},"ExecuteBashTool"," (a validated bash command inside a workspace dir), ",[16,1063,1064],{},"EnvironmentToolset"," for shell and file I\u002FO",[1013,1067,1068,1069],{},"pass a plain function; it gets wrapped as a ",[16,1070,1071],{},"FunctionTool",[12,1073,1074,1075,1078,1079,1081,1082,1084],{},"So the tools port conceptually. Your own tool ",[642,1076,1077],{},"bodies"," port literally — mine are ordinary functions that shell out to git or fetch a URL, and nothing about them is Anthropic-shaped. What does not port is the wrapper: ",[16,1080,1036],{}," is an MCP server, ",[16,1083,1049],{}," wants Zod, ADK wants a typed Python function. Same logic, three adapters.",[12,1086,1087,1088,1091,1092,1095,1096,1099,1100,1102],{},"Which points at the actual lever. ",[38,1089,1090],{},"The more of an agent's capability lives in tools you defined, the less any SDK's built-ins matter."," A model will use a tool you hand it whether or not its SDK ships an equivalent — so a ",[16,1093,1094],{},"run_tests"," or an ",[16,1097,1098],{},"apply_patch"," of your own is portable in a way that depending on ",[16,1101,22],{}," is not. The built-ins are where these three ecosystems differ most; the tools you wrote are where they differ least. If you expect to move, write more of your own than you strictly need to.",[12,1104,1105,1108],{},[38,1106,1107],{},"What actually doesn't port is the control surface",", and that is where the work is:",[862,1110,1111,1127,1137],{},[865,1112,1113,1116,1117,1119,1120,1122,1123,1126],{},[38,1114,1115],{},"The restrict-versus-approve split."," ",[16,1118,631],{}," limits what exists and ",[16,1121,635],{}," skips the permission prompt — two separate ideas in one SDK. OpenAI puts approval on the tool itself with ",[16,1124,1125],{},"needsApproval",". Get this mapping wrong and you will believe you removed a tool when you only stopped it asking, which is the exact mistake earlier in this post.",[865,1128,1129,1116,1132,26,1134,1136],{},[38,1130,1131],{},"The caps.",[16,1133,658],{},[16,1135,661],{}," are Anthropic's, and I would not assume an equivalent exists elsewhere. If it doesn't, the cap moves into your loop — you accumulate cost per turn and break. Your container is the right place for that anyway, since it is the thing that can be killed.",[865,1138,1139,1142,1143,1145,1146,1148],{},[38,1140,1141],{},"How a run reports that it stopped."," This SDK ends the stream with a result whose ",[16,1144,670],{}," says ",[16,1147,678],{},", and does not throw. Another SDK may throw, may return a finish reason, may just stop. Whatever it does, that is the signal your orchestrator has to read before it marks a task complete.",[12,1150,1151,1152,1155],{},"The useful way to think about it: ",[38,1153,1154],{},"the container is the portable abstraction and the SDK contract is not."," Keep the boundary at the HTTP control server — prompt in, events out, killable — and the thing inside becomes swappable. I have not done that swap yet, so treat this as a map rather than a report.",[46,1157,1159],{"id":1158},"what-this-does-not-solve","What This Does Not Solve",[12,1161,1162,1165],{},[38,1163,1164],{},"The Docker socket is real."," The orchestrator can create and destroy containers on your host. Local-only.",[12,1167,1168,1171],{},[38,1169,1170],{},"Containers are isolation, not a sandbox."," A container with your project mounted and network access can reach your network. For untrusted prompts you want no network by default and a stricter runtime than plain Docker.",[12,1173,1174,1177,1178,1181,1182,1184],{},[38,1175,1176],{},"Secrets still flow in."," The API key goes into the container as an environment variable, and a mounted ",[16,1179,1180],{},".env"," is readable by whatever runs inside. An agent with ",[16,1183,29],{}," can read it. Mount what the task needs and nothing else.",[12,1186,1187,1190,1191,26,1193,1196,1197,1199,1200,1202],{},[38,1188,1189],{},"Publish the control port carefully, and put a token on it."," The control server is what makes the follow-up loop work, and it has no authentication of its own. ",[16,1192,778],{},[16,1194,1195],{},"DELETE \u002Fshutdown"," will do what they are told by whoever reaches them, and what they are told goes to an agent running in ",[16,1198,287],{}," mode with ",[16,1201,29],{}," and a writable project mount.",[12,1204,1205,1206,1209,1210,1213],{},"Docker's ",[16,1207,1208],{},"-p 8080:8080"," binds every interface by default, which is the part that catches people out — including me. Write ",[16,1211,1212],{},"-p 127.0.0.1:8080:8080"," and a published port stops being a network service. Better still, put the orchestrator and the agents on one Docker network, address containers by name, and never publish the port to the host at all. Then add the shared secret anyway, because loopback is not a trust boundary on a machine you also run a browser on.",[12,1215,1216,1217,1220],{},"The same applies to the orchestrator itself. It mounts the Docker socket and ",[16,1218,1219],{},"POST \u002Ftasks"," creates containers, so it is the one thing on this list that absolutely must not be reachable from the network.",[46,1222,1224],{"id":1223},"why-bother","Why Bother",[12,1226,1227],{},"Because the container is the unit that makes the rest of it tractable. One task, one container, one mounted directory, one budget. Delete it and the state is gone.",[12,1229,1230,1231,1233],{},"Three of them can run at once without fighting over your working tree. The agent can be given ",[16,1232,697],{}," without you flinching, because you can throw the whole thing away. And a hard budget cap turns \"I left an agent running\" from a story about money into a non-event.",[12,1235,1236],{},"The agent loop itself is twenty lines. Everything else here is deciding what it's allowed to touch, and how you get it back.",[1238,1239,1240],"style",{},"html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":59,"searchDepth":100,"depth":100,"links":1242},[1243,1244,1245,1246,1247,1248,1249,1250,1251,1252],{"id":48,"depth":100,"text":49},{"id":701,"depth":100,"text":702},{"id":765,"depth":100,"text":766},{"id":795,"depth":100,"text":796},{"id":838,"depth":100,"text":839},{"id":918,"depth":100,"text":919},{"id":954,"depth":100,"text":955},{"id":972,"depth":100,"text":973},{"id":1158,"depth":100,"text":1159},{"id":1223,"depth":100,"text":1224},"2026-03-28","An agent that can edit files and run shell commands needs a blast radius. One container per task, a hard budget cap, and a control server inside each container so you can talk to a running agent — built on the Claude Agent SDK.","md","\u002Fimg\u002Fdocker-coding-agents.jpg",{},"\u002Fblog\u002Frunning-coding-agents-in-disposable-docker-containers",{"title":5,"description":1254},{"loc":1258},"blog\u002Frunning-coding-agents-in-disposable-docker-containers",[1263,1264,1265],"AI","Docker","Claude Code","mnWJUfWEauKJzNqSomWzzfkiLUElI1-UADyMXB6aDY4",[1268,1273],{"title":1269,"path":1270,"stem":1271,"description":1272,"children":-1},"Rendering Bhatkhande Notation Without a Font","\u002Fblog\u002Frendering-bhatkhande-notation-without-a-font","blog\u002Frendering-bhatkhande-notation-without-a-font","Bhatkhande notation has always needed a custom font on the web, which means the text is not really text. Swarlipi takes an ASCII grammar and renders real Unicode letters in five scripts, with every mark drawn in CSS and inline SVG.",{"title":1274,"path":1275,"stem":1276,"description":1277,"children":-1},"Running Multiple Local Supabase Projects in Parallel","\u002Fblog\u002Frunning-multiple-local-supabase-projects-in-parallel","blog\u002Frunning-multiple-local-supabase-projects-in-parallel","How to run several local Supabase stacks side by side with the Supabase CLI — static port blocks per project, no port conflicts, no restarting stacks when you switch projects, and Claude Code always talking to the right database.",1790075467950]