Repository navigation
Description
TL;DR
Agents reach for powershell.exe -Command "[System.IO.Ports.SerialPort]::Open(...)" to read device output on Windows because PowerShell is always available. Without explicitly setting DtrEnable=true, RtsEnable=true the LPC11U35 USB-VCOM bridge (and similar USB-VCOM bridges on many MCU dev boards) silently discards every byte the device sends. The host sees zero bytes. The device looks dead. It is not.
This methodology bug burned multiple sessions on #3300 / #3325 chasing a fictitious "ArduinoCore-LPC8xx commit regression." The LPC845-BRK was emitting a perfectly well-formed JSON-RPC Method not found: echo response throughout — and that error is the actual root cause, on every LARGE_MEMORY platform including ESP32-C6. See #3325 update comment.
The project already has the right tool. The agent docs and hooks should make it impossible to bypass it.
Repro
# WRONG — what agents typed (zero bytes seen):
$port = New-Object System.IO.Ports.SerialPort('COM20', 115200, 'None', 8, 'One')
$port.Open()
# … no data ever arrives even though device is transmitting
# RIGHT — what was missing:
$port.DtrEnable = $true
$port.RtsEnable = $true
$port.Open()
# … bytes flowThe right tool in this repo is ci/util/serial_interface.py::_pyserial_dtr_reset (and the wider monitor pipeline that goes through bash autoresearch), which already does the DTR/RTS handshake correctly via pyserial. There is no reason to ever shell out to PowerShell for serial-port work.
Cost of the past two sessions
- [BLOCKER] LPC845-BRK AutoResearch bring-up echo returns empty RX (suspected HardFault) — blocks #3102 #3300 (LPC845-BRK bring-up echo): "0 bytes in 6 seconds after a clean pyocd reset" — was not device silence, was DTR/RTS misconfig.
- [#3300 bisect] Find the commit that broke LPC845-BRK JSON-RPC echo — bisect Arduino-core LPC8xx #3325 (ACLPC commit bisection): planned 10-commit binary search across
4560e6c41d..da8f2b8b42premised on the "device is silent" assumption. Verified during the pivot in [BLOCKER-#3325] LPC PIO build fails on C++14 digit separators in m0clockless_c.h — set -std=gnu++17 + drop digit separators #3329 / revert(lpc): drop -std=gnu++17 overrides; PIO defaults now compile #3331 follow-up that both ACLPC endpoints emit identicalMethod not found: echoresponses — i.e. the entire ACLPC axis was a false trail. - Multiple followup PRs / issues filed ([BLOCKER-#3325] LPC PIO build fails on C++14 digit separators in m0clockless_c.h — set -std=gnu++17 + drop digit separators #3329, revert(lpc): drop -std=gnu++17 overrides; PIO defaults now compile #3331, nxplpc: framework-arduino-lpc8xx pin is hardcoded — platform_packages override in platformio.ini is silently ignored (blocks FastLED#3325 bisection) fbuild#663) on the premise that the LPC build path was the problem.
Cumulative agent time spent: hours. Cumulative useful work delivered: zero on the original bug. Real bug (echo binding not landing in mRegistry on either LPC or ESP32-C6) still untouched at session end.
Three-layer fix
1. Agent docs — explicit forbid + rationale
Add to agents/docs/commands-reference.md (and link from agents/docs/testing-commands.md + CLAUDE.md's "Key Commands" block):
FORBIDDEN: PowerShell
[System.IO.Ports.SerialPort]for device monitoring.USB-VCOM bridges (LPC11U35 on LPC845-BRK, CP2102/CH340 on ESP, FT231X on Teensy, etc.) require
DtrEnable=true, RtsEnable=trueon the host side to gate data through.[System.IO.Ports.SerialPort]::Open()does not set these by default. Bytes are silently dropped. Agents will spend hours blaming firmware for what is a host-side handshake bug.Use one of:
bash autoresearch <board> --timeout 30— full build + flash + monitor cycle with correct DTR/RTS. First choice.uv run --no-project --with pyserial python -c "..."for an ad-hoc probe — pyserial defaults to DTR/RTS correctly.- The internal
ci/util/serial_interface.py::_pyserial_dtr_resethelper for programmatic flows.If you find yourself typing
powershell.exe -Command "[System.IO.Ports.SerialPort", stop. The hook will block it. The right tool is upstairs.
2. PreToolUse hook — make the rule unbypassable
New ci/hooks/forbid_powershell_serialport.py (model after the existing ci/hooks/check_forbidden_commands.py and ci/hooks/protect_example_ino.py). Block any Bash/PowerShell invocation matching:
powershell.*SerialPort(case-insensitive)powershell.*GetPortNamesis OK (enumeration is harmless)System.IO.Ports.SerialPorteven inside a multi-line Heredoc
Error message should name the issue (#XXXX) and point at the doc rule above so the next agent reads it.
Override: FL_AGENT_ALLOW_POWERSHELL_SERIALPORT=1 for the rare debug-the-hook case. Don't reuse FL_AGENT_ALLOW_ALL_CMDS — that's too broad.
3. Memory rule — surface this in auto-memory so future sessions don't relearn
A user-level memory entry (feedback_no_powershell_serial.md) keyed off "USB-VCOM" / "SerialPort" / "DTR/RTS" so future Claude Code sessions in this repo see the rule in their MEMORY.md even before the docs/hook fire.
The body should be one sentence:
Never drive serial-port I/O via
powershell.exe [System.IO.Ports.SerialPort]. Usebash autoresearchor pyserial. PowerShell's SerialPort defaults silently drop DTR/RTS and bytes from the LPC11U35 (and similar bridges) never reach the host. Cost: multiple-session investigations attributed to "device silence" turned out to be "host swallowed the bytes." See #XXXX.
Acceptance criteria
-
agents/docs/commands-reference.mdhas a forbidden-tool block on PowerShell SerialPort. -
ci/hooks/forbid_powershell_serialport.pyexists, is registered insettings.json(or wherever PreToolUse hooks land), and blocks the pattern with a clear message that references this issue. -
ci/hooks/forbid_powershell_serialport.pytest passes for the patterns above. -
CLAUDE.mdcross-references the rule under "Core Rules". - Auto-memory entry seeded so future sessions inherit the rule without re-reading the issue.
Out of scope (split if it grows)
- Generalizing the hook to other "agent reaches for the wrong tool" patterns (e.g. raw
meson/ninja/clang++already blocked bycheck_forbidden_commands.py— same pattern, different namespace). The hook framework is in place; this is just one more rule. ci/util/serial_interface.pyergonomic improvements (anopen_with_handshake(port)convenience). Worth doing separately so the right path is even shorter than the wrong one.
Refs
- [BLOCKER] LPC845-BRK AutoResearch bring-up echo returns empty RX (suspected HardFault) — blocks #3102 #3300 — LPC845-BRK bring-up (mis-diagnosed for two sessions due to this).
- [#3300 bisect] Find the commit that broke LPC845-BRK JSON-RPC echo — bisect Arduino-core LPC8xx #3325 — ACLPC commit bisection plan (false trail invalidated by the same methodology bug).
ci/util/serial_interface.py:_pyserial_dtr_reset— the correct existing implementation.ci/hooks/check_forbidden_commands.py— the pattern to model the new hook after.
Filed by an agent who made the mistake. Twice. In a row.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
fix(hooks): tighten ps+SerialPort regex to require real invocation
fix(autoresearch/lpc): assert DTR=True + VID:PID port detection (#3300)…
ci(hooks): forbidden_commands.py — block pyocd/esptool/dfu-util/picot…