The Steering Wheel Problem: run.steer(), turn/steer and Who May Redirect a Running Agent Cursor shipped run.steer(text), a method that injects a message into an in-flight agent turn and resolves with complete_delivered or revert_to_followup, according to Cursor's SDK changelog as cited in a first-person comparison of four agent-steering tools. The same Cursor release added MCP tool annotations that Cursor describes as descriptive hints passed to the model and not enforced by the SDK, a point the author uses to argue that annotations such as readOnlyHint are not a safety mechanism. The comparison also covers Codex's turn/steer JSON-RPC method requiring an expectedTurnId, Grok Build's Agent Dashboard that queues messages to busy agents, and Alook's shared chat rooms, while the author reports being unable to solve prompt injection through a shared channel. For about a year my relationship with a running coding agent was binary. Either I let it finish, or I hit Escape and lost whatever it was halfway through. Somewhere in the last few weeks that changed across almost every harness I use. Cursor shipped run.steer . Codex has turn/steer. Grok Build has a dashboard where you type into a busy agent. Alook went further and put agents into shared chat rooms where your teammates can talk to them directly. All of these are, at heart, the same feature: you can redirect an agent while it is still working. And the more I played with them, the more I realized the interesting question is not “can I steer?” but “who else can?” and “what does a steer message actually have the authority to do?” This post is my attempt to answer that for myself. I compare how four tools handle it, argue that tool annotations like readOnlyHint are not a safety mechanism, build a small .NET version of steering plus hash-bound approvals against a local Ollama model, and then admit the part I could not solve: prompt injection through a shared channel. I want to be careful here, because a lot of what I read this week was secondhand. I’ll say where I got each thing. I first saw this in the Simplifying AI newsletter on 6 October, then went to Cursor’s own SDK changelog. The short version, from the changelog: run.steer text injects a message into the turn that is already in flight. It resolves with complete delivered when the message landed, or revert to followup when you should send it as an ordinary follow-up instead. Cloud runs return revert to followup, so live steering is a local TypeScript thing for now. Two details stood out to me: The same release added MCP tool annotations, and Cursor is explicit that they are descriptive hints passed to the model and that the SDK does not enforce them. Hold on to that sentence. It’s the whole argument of this post in one line, and it came from the vendor. This is the one I’m least sure about, so let me be upfront. The shape I’ve seen described is a turn/steer JSON-RPC method on the Codex app-server that requires an expectedTurnId. If the turn you think you're steering is no longer the active turn, the steer is rejected instead of silently landing in the wrong place. I confirmed part of this myself: the current codex-rs/app-server/README.md in the openai/codex repo lists "turn start/steer" among the input RPCs that get checked against managed provider requirements, while interrupt stays available even when those checks fail. The exact request shape with expectedTurnId I only saw through third-party mirrors of the repo and through JIN's control-layer reading of the Codex harness "Who Holds the Pen?" . JIN himself warns that the internal type names he read will drift and should be treated as evidence of a pattern rather than a stable API. I'm treating it the same way. Even if the field name changes, the idea is the best one in this whole space, and it’s the one I copied. Grok Build’s Agent Dashboard grok dashboard, /dashboard, or Ctrl+\ shows every session grouped by state: needs input, working, idle, completed, failed. According to xAI's docs, when you select a row and type, an idle agent receives the message immediately and a busy one queues it. Permission prompts can be answered inline with number keys. Ctrl+W dispatches new agents into their own git worktree under ~/.grok/worktrees/. So Grok’s “steer” is really a queue, not an injection. I actually like that. It’s less magical and easier to reason about. And worktrees solve a different but related problem: if two agents are being redirected in parallel, at least they can’t overwrite each other’s files. Alook is the one that made me write this post. It connects the agents already running on your machine Claude Code, Codex, Grok Build, Cursor, OpenCode, Pi to shared rooms with persistent handles, inboxes and channel memberships. Teammates can ask your agent for a change without you relaying the message. Their FAQ says bot-owner consent gates who can add your agent to a server, membership decides who can address it, and a mention never silently expands the agent’s permissions. That last promise is a good one. But “who can address it” is exactly the steering question, and in a room, the answer is “everyone in the room.” This is my reading as of today. Treat the Codex row as partly verified, and the “who” column as describing the default, not every possible configuration. +----------------+----------------------+------------------------+------------------+-----------------------+| Tool | Steer mechanism | Who can steer | Stale-turn guard | Approvals live in |+----------------+----------------------+------------------------+------------------+-----------------------+| Cursor SDK | run.steer text , | Whoever holds the run | No explicit id; | Your SDK host code || | injects into the | handle in your process | falls back to | annotations are || | live turn local TS | your code | revert to follow | hints only |+----------------+----------------------+------------------------+------------------+-----------------------+| Codex | turn/steer JSON-RPC | Any client connected | expectedTurnId | App-server approval || app-server | on the active turn | to the app-server | partly verified | requests to client |+----------------+----------------------+------------------------+------------------+-----------------------+| Grok Build | Type in dashboard: | The local user at the | Implicit: busy | Inline prompts in the || dashboard | idle gets it now, | terminal | agents queue | dashboard || | busy queues it | | | |+----------------+----------------------+------------------------+------------------+-----------------------+| Alook | Message the agent in | Any member of a room | Not documented | Agent's own runtime || | a channel or DM | the agent belongs to | that I found | on the owner's box |+----------------+----------------------+------------------------+------------------+-----------------------+| My .NET demo | POST /steer between | Owner plus an explicit | expectedTurnId, | Harness, bound to a || below | model round-trips | per-thread allowlist | 409 on mismatch | SHA-256 payload hash |+----------------+----------------------+------------------------+------------------+-----------------------+ The column that worries me is “who can steer.” It went from “me” to “my code” to “anyone connected to the server” to “anyone in the room” in about a month, and the approval column didn’t move with it. MCP lets a server describe a tool with annotations such as readOnlyHint and destructiveHint. They're useful. A well-behaved model will be more careful with a tool marked destructive, and a UI can show a red badge next to it. But think about who writes them. The annotation is a claim the tool server makes about itself. The model reads it as text. Nothing between “the model decided to call delete file" and "the file is gone" checks that claim. Cursor's changelog says so plainly: hints for the model, not enforced by the SDK. That leaves the harness as the only place enforcement can live. Several things I read this month landed on the same conclusion from different directions: Roth’s approval pattern is good, but it leaves one gap that Giri’s hash closes. An approval in Microsoft.Extensions.AI is tied to a call id. If anything between the approval card and the execution can change the arguments a remote UI, a proxy, a buggy client that rebuilds the call , the human approved one thing and the tool runs another. Binding the approval to a hash of the exact payload, and checking that hash at execution time inside the harness, turns “the human clicked yes” into “the human said yes to these bytes.” Here’s what I wanted from a small prototype: Local model with tool calling. qwen3:8b worked best for me among small models; llama3.1:8b is a reasonable alternative.ollama pull qwen3:8bollama serve if it's not already runningdotnet new web -n SteerDemocd SteerDemodotnet add package Microsoft.Extensions.AIdotnet add package OllamaSharp A note before the code: ApprovalRequiredAIFunction and the approval content types are marked experimental in Microsoft.Extensions.AI diagnostic MEAI001 . The names have been stable across the previews I've used, and Roth's September post still uses them, but expect some churn. pragma warning disable MEAI001 // approval APIs are experimentalusing System.Collections.Concurrent;using System.Security.Cryptography;using System.Text;using System.Text.Json;using System.Text.Json.Nodes;using System.Threading.Channels;using Microsoft.Extensions.AI;using OllamaSharp;var app = WebApplication.CreateBuilder args .Build ;var fs = new VirtualFs ;var ledger = new ApprovalLedger ;var threads = new ConcurrentDictionary