An evaluation of the copilot-codeact-plugin
("CodeAct") for the GitHub Copilot CLI, benchmarked on a real Windows machine and in WSL.
CodeAct collapses many tool calls into one sandboxed Python run so the conversation context is
replayed across fewer turns.
Measured on 2026‑06‑18 against
@xavierxmorris's setup (Copilot CLI v1.0.64 on Windows, v1.0.63 in WSL Ubuntu 24.04, Python 3.14 / 3.12, Monty backendpydantic-monty0.0.18). Every number came from the plugin's owntests/run_tests.py perfharness against a copy of this repo. The findings were cross‑checked by two independent review passes (GPT‑5.5 and Claude Opus 4.8), which corrected the original failure attribution — see Caveats & method at the end.
- Don't adopt it on native Windows today. As shipped it is broken on Windows (the dispatch is
Unix‑only) and makes things ~5–6× more expensive. A small port (below) fixes the dispatch and
takes roughly a third off the damage (−501% → −325% input) — but it's still net‑negative, because the way CodeAct passes inline
Python (
--code '<python>') is fragile under PowerShell. - The mechanism is real: given a correctly‑passed program, the Monty sandbox inventoried the whole working tree in one call. In a clean environment (WSL) it ran exactly as designed — 2 turns, no thrashing.
- But on small repos (~20 files) it didn't reduce output tokens even when it worked (input/total weren't measurable in WSL), and in a heavy MCP/agent setup a single sandbox hiccup cascades into a 10–16‑turn thrash.
- The payoff only appears for large fan‑out (30+ files) in a lean context — which matches the plugin author's published 49–69% numbers, not everyday work.
Normally the agent loops model → tool → model → tool …, and every turn re‑sends the whole context
(system prompt, tool catalogs, prior messages). CodeAct instead has the model write one Python
program that chains view/glob/grep/bash/etc. and runs it in a sandbox
(Pydantic Monty — a tiny Rust Python interpreter). Fewer turns →
the heavy context is replayed fewer times. With MCP servers loaded, each catalog adds to that per‑turn
context, so in theory the savings compound.
Two read‑only fan‑outs (read every file, parse it, aggregate). The harness runs each prompt twice —
baseline (plain Copilot, no plugin) and codeact (plugin loaded) — and reads token counts from
Copilot's process logs. Read Caveats & method before quoting any single number — the arms aren't
perfectly symmetric and n = 1 per cell.
Same prompt (md-heading-index, 9 markdown files), as shipped vs after my full port — both rows are
saved harness results (native-as-shipped-md.json,
native-ported-md.json), and the two baselines are
near‑identical, so it's a clean comparison:
| Arm | Input tok | Turns | Tools | Est. cost* | Δ input |
|---|---|---|---|---|---|
| baseline (as‑shipped run) | 112,232 | 3 | 2 | $0.30 | — |
| codeact — as shipped | 674,449 | 16 | 15 | $1.75 | −501% |
| baseline (fully‑ported run) | 112,825 | 3 | 2 | $0.30 | — |
| codeact — fully ported | 479,926 | 11 | 10 | $1.26 | −325% |
The port cut codeact's input blow‑up from −501% to −325% and turns from 16 to 11 — better, but
still ~4× baseline. And it's highly variable: across saved runs the codeact arm ranged 10–16
turns and roughly −325% to −501% input — never positive. The second prompt (all-files-inventory, ~20
files) corroborates: on an intermediate port it was −482% input (2 → 10 turns,
native-intermediate-allfiles.json).
*Cost is a normalised GPT‑5.4 basis (see Caveats); the % deltas are model‑independent, the
dollar figures are not.
What the model actually did: it did invoke codeact (codeact_invoked: true in the saved run), but
the sandbox call failed (next section), and it then thrashed — probing its other tools
(tool_search, a SELECT 1 SQL probe, a sub‑agent, shell echos) for 11–16 turns instead of fixing
the program.
| Prompt | Arm | Output tok | Tools | Turns | Δ output | Δ turns |
|---|---|---|---|---|---|---|
| md‑heading‑index | baseline | 730 | 3 | 3 | — | — |
| codeact | 856 | 2 | 2 | −17% | −33% | |
| all‑files‑inventory | baseline | 846 | 2 | 2 | — | — |
| codeact | 1,095 | 2 | 2 | −29% | 0% |
Here CodeAct ran cleanly in 2 turns — no thrashing, collapsing one task from 3 turns to 2. It still didn't reduce output tokens (it has to write the program, which adds output tokens); whether it saved on input/total here is unknown — those weren't captured. Input tokens couldn't be read in the WSL build — Copilot 1.0.63 logs the field differently than 1.0.64 — so this uses output tokens, turns, and tool calls. This run also used a different default model (see Caveats).
Run directly with the real python, the Monty sandbox inventoried the entire working tree (path,
line count, keyword check, sorted) in one execution. The engine is sound; the problems are
everything around it.
Two layers fail. Both were reproduced on this machine.
Layer 1 — the dispatch is Unix‑only (the plugin never runs at all):
python3is the broken Microsoft Store stub. On stock Windows,python3is an app‑execution alias that just prints "Python was not found…". Every install/dispatch script callspython3. The real interpreter here ispython(C:\Python314).scripts/codeactis a bash script. Copilot on Windows runs tool commands through PowerShell, and the defaultbashhere is WSL — a different Linux environment with its own Python and nouv. So the bash dispatch can't run as written.- The hook is wired bash‑only.
hooks.jsonregisters./hooks/pre-tool-use.sh; the bundledpre-tool-use.ps1isn't referenced.
Layer 2 — even with the dispatch fixed, passing the program is fragile (this is the real killer):
The instructions tell the model to run … codeact.py --code '<python>'. Under PowerShell a
single‑quoted argument breaks two different ways, both verified here:
- Inner single quotes split the argument.
--code 'print(f"{x if c else 'na'}")'→ PowerShell passes two tokens and the backend errors withunrecognized arguments: na}")— Monty never runs. - The model's escape‑hatch also fails. To dodge that, the model used escaped double quotes
(
f"{\"yes\" if … else \"no\"}"); Monty reads the\as a line continuation →MontySyntaxError: Expected a newline after line continuation character.
Either way the sandbox returns nothing, and in this heavy setup the failure cascades into thrashing.
The robust fix is to pass the program via --code-file (write the program to a temp file, then run
python …codeact.py --auto --code-file prog.py) instead of a single‑quoted --code arg — that avoids
the shell entirely. (--stdin exists but expects a JSON payload, not raw Python.) The port below does
not yet do this, which is why native stayed
net‑negative.
A small, Windows‑only port in a local clone (the Unix path is untouched):
| File | Change | Effect |
|---|---|---|
scripts/preflight.ps1 |
python3 → python; check $LASTEXITCODE instead of a dead try/catch |
preflight passes on Windows |
scripts/install-instructions.ps1 |
python3 → python; substitute {{BACKEND_LIMITATIONS}} (was left raw → the model never got the Monty rules); rewrite the Invoke line in both the instructions and agent files to python "<dir>\skills\monty-codeact\scripts\codeact.py" … --code '<python>' |
this is the actual dispatch fix — the model invokes the backend on turn 1 |
scripts/codeact.cmd (new) |
a manual wrapper that resolves the backend and runs it with the real python (auto‑installs pydantic-monty) |
convenience only — not on the model's path (the install rewrite calls python …codeact.py directly) |
This fixes the dispatch (proven: correct invocation on turn 1) and adds the Monty guidance, which
cut the worst of the blow‑up (−501% → −325% input, 16 → 11 turns) but didn't make it positive. It is not enough for a positive result, because of Layer 2 (the
--code '<python>' quoting) and the thrash‑on‑failure behaviour below. A genuinely robust port would
switch the Invoke line to --code-file.
Verified in the installed pydantic-monty 0.0.18 — and note the plugin's own SKILL.md is stale
here (it lists things that actually work):
- Works: f‑string format specs (
f"{5:>5}"→␣␣␣␣5,f"{x:.2f}"), lambdas,sort(key=…). - Fails:
str.format(),class,match/case,os.path/os.walk, and backslash escapes inside f‑strings (f"{\"a\"}"). Capable models still trip these, and the sandbox errors. (Set comprehensions,startswith(tuple), and format specs all work — the plugin'sSKILL.mdlists some of these as broken, but they aren't in 0.0.18.)
- Modern Copilot batches well. Baseline finished these in 2–3 turns, so there were few round‑trips to collapse. (The plugin targets ≥8 files and says <5 files isn't worth it; these repos are ≥8, so CodeAct was indicated — it just had little to gain.)
- CodeAct adds output tokens — the model has to write the program. On a small fan‑out that meets or exceeds the reads it saves.
The sandbox can reach MCP servers, but narrowly. The plugin ships an mcp_call() bridge
(scripts/mcp-bridge.py):
- It registers
mcp_callonly when a workspace MCP config declares servers (.mcp.json,.vscode/mcp.json, or.github/copilot/mcp.json). In this eval there was none, somcp_callwasn't even in the tool list. - It does not reuse Copilot's already‑loaded MCP sessions, and it re‑spawns stdio servers with a
stripped environment — so HTTP servers like
microsoft-learnare bridgeable, but token‑authenticated ones likebluebirdgenerally won't work.
So most of your live‑docs / code‑search turns get no benefit unless you re‑declare those servers per‑project — and even then, only the ones that survive a stripped env.
- Large fan‑out — 30+ files, bulk edits, full‑project indexing — where baseline genuinely needs many sequential reads.
- Built‑in tools —
view,create,edit,glob,grep(needsrg),bash,sql,web_fetch,github_api, plus the narrowmcp_callbridge above. - A lean context and a Unix‑like shell (Linux/macOS/WSL, or a Windows port that uses
--code-file).
The author's 49–69% input‑token reductions were measured on a 30+ file fixture — exactly that niche. Treat them as directional (a community, experimental plugin), not as everyday savings.
Skip it on native Windows for now. If you want it:
- Run Copilot in WSL/Linux, or apply the port above (and switch the Invoke line to
--code-file). - Reserve it for genuine 30+ file, built‑in‑tool batch jobs — not routine work.
- Keep MCP lean during those runs (the catalogs are both the context it saves and the trap it thrashes into on failure).
For day‑to‑day token savings, the levers in docs/token-efficiency-guide.md
(caching, scoped @file, subagents, trimming unused MCP tools, lower effort for routine work) are both
safer and bigger than CodeAct.
The comparison is honest but not perfectly controlled — quote it with these in mind:
- Asymmetric arms. The harness runs baseline with
--no-custom-instructionsand no plugin, but the codeact arm keeps custom instructions on, loads the plugin, and appends "Use codeact…". So part of codeact's per‑turn input is context the baseline never paid for. - Cost basis is fixed GPT‑5.4 pricing (
input×$2.50/M + output×$15/M). Native here likely ran the documented Opus default, so actual native cost is several× the figures shown. The % input/turn deltas are model‑independent; the dollar amounts are a normalised proxy. - Different builds/models per environment. Native = Copilot 1.0.64 (Opus‑class default); WSL = 1.0.63 (Sonnet‑class default). WSL's clean 2‑turn run reflects both a leaner context and a lighter model — don't attribute it to context alone.
- Tiny sample. Two prompts,
n = 1per cell, on a ~20‑file repo; baselines are non‑deterministic (onemd-heading-indexbaseline timed out). Treat the magnitudes as directional. The direction is solid for native (net‑negative across every run); for WSL the honest read is operationally clean (2 turns, no thrash) but output‑token‑negative — total/input cost there is unknown (not captured). Raw results:codeact-bench-results/. - Input tokens were captured on Windows (1.0.64) but not in WSL (1.0.63 log format).
git clone https://github.com/jsturtevant/copilot-codeact-plugin
python -m pip install uv pydantic-monty # uv + the Monty backend
# (Windows) apply the port: python3 -> python in scripts\preflight.ps1 and
# scripts\install-instructions.ps1, substitute {{BACKEND_LIMITATIONS}}, and rewrite the
# Invoke line to call python ...codeact.py (ideally via --code-file). See the port table above.
powershell -File plugins\codeact\scripts\install-instructions.ps1 -Backend monty
python plugins\codeact\tests\run_tests.py perf `
--prompts prompts.json --workspace <copy-of-repo> `
--plugin-dir plugins\codeact --min-token-reduction 0Each prompt runs Copilot twice (~2 premium requests/prompt); always benchmark a copy (the harness
uses --yolo).
- CodeAct plugin & published benchmarks — https://github.com/jsturtevant/copilot-codeact-plugin
- Pydantic Monty (default backend) — https://github.com/pydantic/monty
- CodeAct pattern background — https://devblogs.microsoft.com/agent-framework/codeact-with-hyperlight/