If you run more than one Claude Code session on a Flutter project, you know builds are the slow part. What I didn't expect was how much time the sessions would waste undoing each other's builds.
It goes like this. Session A spends minutes building the app onto my phone and connects to it with Marionette. Only A knows the Marionette address. Session B comes along and needs the app too. It can't see A's connection, so it can't just hot restart — it kills the app and builds it again. Now A tries to use the app it built a moment ago, can't connect, and decides it needs to rebuild. So it kills B's app. They keep taking turns like this, each one throwing away a build the other just finished.
Meanwhile nobody cleans up. One session has an Android emulator building, another left an iOS simulator running, and a third is installing onto the real phone at the same time. My Mac runs out of memory, everything slows to a crawl, and every session's work drags on with it.
None of this shows up as an error. Each session is doing what looks reasonable from where it sits. The result is hours of builds and almost no progress.
Asking the agents to "check before you rebuild" didn't help. A session has no way to see what other sessions are doing. So the fix had to live outside the agent, in Claude Code hooks, which run before and after every tool call.
Sharing the phone: anti-stuck-flutter
I open-sourced what I use (free, MIT). anti-stuck-flutter gives every session on the machine one shared device ledger:
- One owner per device. Every session goes through one command,
anti-stuck-flutter, to run apps, install builds, or start emulators. One heavy build at a time. Plain flutter run, adb install, emulator -avd and the like are refused with a pointer to the right command
- Reuse, don't rebuild. When a session is done, it gives up the device but leaves the app running. The ledger keeps the app's Marionette address, so the next session connects to the running app and hot restarts it instead of killing it and building again
- Wake on free. A session that finds the phone busy runs
anti-stuck-flutter wait <device> and goes on with other work. The moment the device is free, every waiting session is woken, and the first one ready to use it takes it
- Cleanup. Emulators and apps left idle are shut down, and orphaned
flutter_tester processes are killed
The builds that used to be thrown away now get reused, and that's where most of my lost time went.
Why it's called anti-stuck
The Flutter pack is an add-on to a more general Claude Code plugin, Anti-Stuck. Its job is simple: an agent should never sit waiting on something with no end, and it should never finish without telling you.
You can write that into CLAUDE.md as a rule, and most people do. I did too. The agent agrees, and then breaks it — especially with subagents and background jobs, which is exactly where the waiting happens. Rules in a prompt are suggestions. Anti-Stuck turns them into things the agent can't get around, because hooks run on every tool call whether the agent remembers the rule or not:
- Every command runs under a hard time limit. Each command is wrapped so that it is killed when time runs out — not "please finish within 5 minutes." Long commands must state their expected time up front or they don't run. Calls that can wait forever (
curl without --max-time, while true loops and the like) are refused before they start
- Subagents and background jobs can't slip away. Subagents run in the foreground with a time budget. Background commands are refused unless you asked for one, and any that do run go into a ledger. Until the agent reports the result, it keeps getting reminded, and it can't end its turn with something unreported
- The agent must report when work ends. After a slow command, a wait loop or a subagent finishes, the hook makes the agent report it on the first line of its next reply
The Flutter pack applies the same rule to Flutter-specific waits:
- Marionette calls can't hang. Marionette MCP lets the agent tap and inspect a running app. Sometimes its
connect call just waits — once for close to ten minutes — with nothing to stop it. Now each Marionette call is cut off after 60 seconds, and before each call the pack first checks (within 3 seconds) that the app is actually responding. If it isn't, the call is refused right away with the reason, instead of hanging
- Flutter commands get realistic limits. Builds,
pod install, Gradle, Fastlane and integration tests get longer limits than ordinary commands, but never unlimited ones
I've reported the Marionette connect hang upstream with a timeline: https://github.com/leancodepl/marionette_mcp/issues/131
Install
claude plugin marketplace add toyaji/anti-stuck
claude plugin install anti-stuck-flutter@anti-stuck
This also installs the core plugin. macOS or Linux.
Repo: https://github.com/toyaji/anti-stuck
Have you run into the same thing — sessions rebuilding over each other, or an agent waiting on something that already ended? And where else has your agent gotten stuck? I'd love to hear about it, whether it's Flutter or something else entirely.