Skip to content

Shell command execution events

Shell execution begins after the generic tool pipeline has selected a command tool such as bash or powershell. From there, the shell manager chooses the PTY or process backend, tracks sync/async/detached commands, buffers large output, forwards input, sends completion notifications, and shuts down shell sessions.

Read Built-in tools, execution events, and results for the generic tool lifecycle before the shell callback. Read Sandbox implementation for MXC policy enforcement. Read Terminal setup and shell environment for keyboard/multiline setup, which is intentionally separate from command execution.

The important implementation point is that terminal setup, shell tool registration, and shell process execution are three different layers. /terminal-setup only configures multiline keyboard input in the user’s terminal. The command tools are created later from session tool configuration and run through a ShellToolManager-style object that owns shell sessions and task state.

Shell layerOwned here?Notes
Terminal keybinding setupnoRuntime lifecycle concern; affects human input only.
Shell tool registrationpartlyTool names are assembled in runtime tool assembly; shell manager provides the suite.
Process executionyesPTY/process backend, queues, task state, output buffering, and cancellation.
Sandbox policyadjacentShell execution calls into sandbox adapters when enabled; detailed policy lives in sandboxing.md.

Because app.js is bundled/minified, symbol names are unstable. Line references below are searchable anchors in this extracted build and will shift across releases.

Source anchors

Semantic aliasMinified anchorApprox. app.js lineRole
Shell configu0 / ShellConfig5390Defines Bash and PowerShell tool names, descriptions, safety assessor, sandbox config, init profile, and process flags.
Large output bufferBW555Keeps command output in memory until a threshold, then spills to a session temp file with a preview.
Large-output message formatterkMe(...), Nlo(...)563, 5662Formats “Output too large” messages and points the model to the saved output file.
Interactive PTY sessiondve / InteractiveShellSession5640Long-lived TTY-backed shell with xterm parsing, command markers, input writing, output reading, and shutdown.
Shell managermCt / shell context manager5666Creates shell/read/write/stop/list tools and tracks shell sessions, task metadata, detached sessions, queues, and promotion state.
Command schemasvCr, Rlo, ICr, Plo5666Shell command, write-input, read-output, and stop/list input schemas.
Command execution callbackexecuteShellToolCallback(...)5676Dispatches sync/async/detached execution, registers tasks, sets partial-output callbacks, and handles timeout/background transitions.
Background promotionpromotableSyncShells, syncShellPromotionResolvers5682-5683Allows a running sync command to be promoted to background from the UI/session task layer.
Non-TTY process sessionhCt / process shell session5685Feature-gated backend that spawns one child process per command and does not support terminal input.
Shell tool assemblyWjs(...)5734Chooses PTY vs non-TTY backend and returns the shell, read, write, stop, and list tool definitions.
Session task projectiongetBackgroundTasks, promoteTaskToBackground4471Merges shell tasks with agent tasks for UI/background-task APIs.

Runtime assembly

Shell tools are assembled lazily from the session tool configuration. The assembly path around Wjs(...) checks whether toolConfig.shellContext already exists. If not, it builds one and stores it back on the config so later tool initialization shares the same shell context.

flowchart TD
ToolInit[Tool initialization] --> Existing{"shellContext already exists?"}
Existing -->|yes| Reuse[Reuse existing ShellToolManager]
Existing -->|no| Config[Resolve shellConfig or default Bash]
Config --> Backend{"sandbox disabled and SHELL_SPAWN_BACKEND enabled?"}
Backend -->|yes| ProcessFactory[Non-TTY process session factory]
Backend -->|no| PtyFactory[Interactive PTY session factory]
ProcessFactory --> Manager[Create shell manager]
PtyFactory --> Manager
Manager --> Tools[Return shell/read/write/stop/list tools]

Backend selection has two notable consequences:

BackendCapability flagsConsequence
Interactive PTY (dve)ttySupport: true, terminalInput: trueExposes the write-input tool, can keep shell state across async commands, and supports interactive programs.
Process spawn (hCt)ttySupport: false, terminalInput: falseRuns each command in a fresh child process, omits the write-input tool, and warns that shell state does not persist.

The non-TTY backend is only selected when sandboxing is disabled and the SHELL_SPAWN_BACKEND feature flag is enabled. When sandboxing is enabled, the shell path stays on the PTY/session factory because the sandbox adapter lives on that branch.

Tool suite

The shell manager generates a small family of tools from the active ShellConfig.

Shell typeExecuteReadWriteStopList
Bashbashread_bashwrite_bashstop_bashlist_bash
PowerShellpowershellread_powershellwrite_powershellstop_powershelllist_powershell

write_* is only emitted when the backend advertises terminalInput: true. read_*, stop_*, and list_* are emitted for both backends.

The execute tool schema includes:

FieldMeaning
commandShell command text.
descriptionShort human-readable summary used for task lists and notifications.
shellIdOptional identifier for an async session. Reusing a shell ID reuses the session when the backend supports it.
modesync by default, or async for background execution.
detachOptional async-only path that starts a fully detached process and logs output to a file.
initial_waitSync wait budget in seconds before a still-running command is moved to background.

Main execution flow

The shell manager serializes work per shellId with an execution queue. This prevents two commands from being injected into the same shell at the same time.

sequenceDiagram
participant Model
participant Tool as Shell tool callback
participant Queue as Per-shell queue
participant Manager as Shell manager
participant Session as Shell session
participant Tasks as TaskRegistry
participant UI as UI/system events
Model->>Tool: command, description, mode, shellId, initial_wait
Tool->>Queue: enqueue by shellId
Queue->>Manager: executeShellToolCallback
Manager->>Session: getOrCreateSession(cwd, shellId)
Manager->>Tasks: register/update shell task
alt async + detach
Manager->>Session: tryExecuteDetachedCommand(command, logPath)
Session-->>Manager: spawned and released
Manager->>Tasks: detached shell task with logPath
else async attached
Manager->>Session: tryExecuteAsyncCommand(command)
Session-->>Manager: shellId and initial output
Manager->>Tasks: running attached background task
else sync
Manager->>Session: executeCommand(command, initial_wait)
alt completed before timeout
Session-->>Manager: output + exitCode
Manager->>Manager: shutdown sync session
else timeout or user promotion
Manager->>Tasks: mark as background + retain task
Session-->>Manager: shell remains attached
end
end
Session-->>UI: partial output / completion callbacks

The task state stored by the manager includes the command text, description, execution mode, started/completed timestamps, PID, status, notification preference, and whether the task should remain visible after completion.

Sync, async, promotion, and detach

The shell tool has four materially different behaviors.

ModeAttachmentSession behaviorTask behavior
sync completes before initial_waitAttachedRuns command, reads final output, then shuts down sync session.Usually removed from active shell tasks after completion.
sync exceeds initial_waitAttachedLeaves command running in the session.Converts to background shell task and may notify on completion.
sync promoted by userAttachedResolver returns early with a model-hidden “moved to background” message.Marks the same task as background and retained.
asyncAttachedStarts command and returns a shellId; session remains available for read_*, write_*, and stop_*.Registered as running background shell task.
async + detachDetachedStarts an independent OS process, writes PID/log files, then releases session resources.Registered as detached task with logPath; progress is read from the log file.

The promotion path is implemented with promotableSyncShells and syncShellPromotionResolvers. The session-level background-task APIs consult shell promotion first-class alongside agent task promotion, which is why a timed-out or promoted command can appear in the same UI surface as a background subagent.

Interactive PTY backend

The PTY-backed session keeps a real shell process alive and wraps each command with markers.

flowchart TD
Create[Create PTY shell] --> Init[Initialize shell environment]
Init --> Wrap[Wrap command with output and done markers]
Wrap --> Send[Send command text to PTY]
Send --> Xterm[xterm parser normalizes terminal output]
Xterm --> Buffer[Large-output buffer]
Buffer --> Read[readOutput]
Read --> Marker{"done marker observed?"}
Marker -->|no| Poll[Continue polling / allow read tool]
Marker -->|yes| Result[Return output and exit code]

Observed implementation details:

  • The session uses an xterm Terminal object with zero scrollback to parse terminal data and respond to terminal control queries.
  • Raw ANSI sequences are stripped/normalized before appending model-visible output.
  • Commands are wrapped with a printed-output marker and a ___BEGIN___COMMAND_DONE_MARKER___<exitCode> completion marker.
  • The output buffer ignores shell preamble before the printed-output marker and ignores content after the done marker.
  • PowerShell receives a small initialization profile that simplifies the prompt and sets UTF-8 output defaults.
  • Bash initialization clears prompts and history state; in non-interactive profile mode it can source BASH_ENV before clearing prompt variables.
  • write_* uses trySendInput(...) and converts text or key notation into terminal input, then read_* retrieves the resulting output.
  • On Windows shutdown, a running PTY command triggers a process-tree kill through taskkill.exe; otherwise the PTY process is killed directly.

The PTY backend is the only path that can support interactive command-line programs because it owns a TTY and can send input after the command starts.

Non-TTY process backend

The process backend is a separate session implementation. It spawns a new child process for each command instead of injecting commands into a long-lived TTY.

flowchart TD
Execute[executeCommand] --> Spawn[spawn child process]
Spawn --> Stdout[stdout pipe]
Spawn --> Stderr[stderr pipe]
Stdout --> Buffer[Large-output buffer]
Stderr --> Buffer
Spawn --> Exit[exit/close event]
Exit --> Complete[buildOutput + commandCompleteCallback]

Key differences from the PTY backend:

  • trySendInput(...) always returns false, so no write-input tool is exposed.
  • Bash commands are run as bash --norc --noprofile -c <command> using the shell config process flags.
  • PowerShell commands are run with -NonInteractive -Command and wrapper logic that maps $?/$LASTEXITCODE to the process exit code.
  • stdout and stderr are appended into the same output buffer.
  • Partial-output callbacks are throttled to roughly 100 ms.
  • On Unix shutdown, the runtime sends SIGTERM to the process group and follows with SIGKILL after 5 seconds.

This backend is useful for simpler, isolated command execution. It is less suitable for commands that require shell state, REPL input, TTY control, or follow-up interaction.

Large output handling

Both shell backends use the same BW output buffer. The buffer starts in memory and switches to a temp file when output exceeds the configured threshold.

flowchart TD
Output[Command output chunk] --> Threshold{"under maxOutputSizeBytes?"}
Threshold -->|yes| Memory[Append to memory buffer]
Threshold -->|no| File[Switch to temp file]
File --> Preview[Maintain preview/tail]
Memory --> Snapshot[readOutput/buildOutput]
Preview --> Snapshot
Snapshot --> Result{"large output?"}
Result -->|no| Inline[Return full output]
Result -->|yes| Saved[Return preview + largeOutputFilePath + total bytes]

The formatter reports that output was saved and suggests using grep/read tools against the saved file. For detached commands, the primary long-running output location is the detached log file (copilot-detached-<shellId>-<timestamp>.log) plus a sibling .pid file.

Read, write, stop, and list tools

The supporting shell tools are thin adapters over manager/session state.

ToolMain behavior
read_*Waits for delay, reads output from an attached session, or reads detached progress from the log path. Invalid delay values are rejected.
write_*Sends input to a running attached PTY session, then reads output after a delay. Not available for non-TTY backend.
stop_*Stops an attached session by shutting down the shell, or cancels a detached task by using its registered PID/log metadata.
list_*Lists active shell sessions/tasks with shell ID, command, mode, PID, status, attachment mode, and unread-output hints.

The manager tracks three overlapping views:

State map/setMeaning
sessionsLive shell session objects keyed by shell ID.
currentExecutionsCommands currently running or recently completed in attached sessions.
detachedSessionsShell IDs whose command was detached and no longer has an attached session.
retainedAttachedShellTasksCompleted attached tasks that remain visible because they were async, timed out, or promoted.
recentShutdownsShort-lived diagnostics for explaining later read/stop attempts against a missing shell ID.

Completion notifications

When background-task notifications are enabled, the manager wires a command-completion callback into each session. Completion updates the tracked task and can trigger a model-visible system notification such as shell_completed or shell_detached_completed through the broader session event/UI projection path.

The important nuance is that notifications are tied to task state, not to polling. A background command can finish without the model repeatedly calling read_*; the next turn can include a system notification telling the model that output is ready.

Error classification

The manager does more than return “shell not found.” For read/stop/list operations it classifies missing or stopped shells using recent shutdown information.

Examples of categories include:

  • unknown shell ID;
  • shell was recently shut down after completion;
  • shell was shut down because of an error or cancellation;
  • detached task has log/PID state instead of a live attached session.

The classification result becomes both a model-visible error message and telemetry fields such as shell_error_category, shell_operation, and a restricted diagnostic summary.

Relationship to other docs

Created and maintained by Yingting Huang.