| title | Shell integration in the Windows Terminal |
|---|---|
| description | In this tutorial, you learn how to configure your shell to enable shell integration features in the Windows Terminal |
| author | zadjii-msft |
| ms.author | migrie |
| ms.date | 12/18/2025 |
| ms.topic | tutorial |
- Shell Integration
Starting in Terminal 1.15 Preview, the Windows Terminal has started experimentally supporting some "shell integration" features. These features make the command-line easier to use. In earlier releases, we enabled shell to tell the Terminal what the current working directory is. Now, we've added support for more sequences to allow your shell to semantically describe parts of the terminal output as a "prompt", a "command", or "output". The shell can also tell the terminal whether a command succeeded or failed.
This is a guide to some of the shell integration features we've rolled out as of Terminal v1.18. We're planning on building even more features on top of these in the future, so we'd love to get some additional feedback on how folks using them.
Note: As of Terminal 1.21, marks are now a stable feature. Prior to 1.21, marks were only enabled for Preview builds of the Terminal. If you're using a version of Terminal before 1.21, the
showMarksOnScrollbarsetting was namedexperimental.showMarksOnScrollbar, andautoMarkPromptswas namedexperimental.autoMarkPrompts.
Shell integration works by having the shell (or any command line application) write special "escape sequences" to the Terminal. These escape sequences aren't printed to the Terminal - instead, they provide bits of metadata the terminal can use to know more about what's going on in the application. By sticking these sequences into your shell's prompt, you can have the shell continuously provide info to the terminal that only the shell knows.
For the following sequences:
OSCis the string"\x1b]"- an escape character, followed by]STis the "string terminator", and can be either\x1b\(an ESC character, followed by\) or\x7(the BEL character)- Spaces are merely illustrative.
- Strings in
<>are parameters that should be replaced by some other value.
The relevant supported shell integration sequences as of Terminal v1.18 are:
OSC 9 ; 9 ; <CWD> ST("ConEmu Set working directory") - Tell the Terminal what the current working directory is.CWDneeds to be a Windows filesystem path for this to work. If using this in WSL or cygwin, you'll need to usewslpathorcygpath.OSC 133 ; A ST("FTCS_PROMPT") - The start of a prompt.OSC 133 ; B ST("FTCS_COMMAND_START") - The start of a commandline (READ: the end of the prompt).OSC 133 ; C ST("FTCS_COMMAND_EXECUTED") - The start of the command output / the end of the commandline.OSC 133 ; D ; <ExitCode> ST("FTCS_COMMAND_FINISHED") - the end of a command.ExitCodeIfExitCodeis provided, then the Terminal will treat0as "success" and anything else as an error. If omitted, the terminal will just leave the mark the default color.
Supporting these features requires cooperation between your shell and the Terminal. You'll need to both enable settings in the Terminal to use these new features, as well as modify your shell's prompt.
To enable these features in the Terminal, you'll want to add the following to your settings:
"profiles":
{
"defaults":
{
// Enable marks on the scrollbar
"showMarksOnScrollbar": true,
// Needed for both pwsh, CMD and bash shell integration
"autoMarkPrompts": true,
// Add support for a right-click context menu
// You can also just bind the `showContextMenu` action
"experimental.rightClickContextMenu": true,
},
}
"actions":
[
// Scroll between prompts
{ "keys": "ctrl+up", "command": { "action": "scrollToMark", "direction": "previous" }, },
{ "keys": "ctrl+down", "command": { "action": "scrollToMark", "direction": "next" }, },
// Add the ability to select a whole command (or its output)
{ "command": { "action": "selectOutput", "direction": "prev" }, },
{ "command": { "action": "selectOutput", "direction": "next" }, },
{ "command": { "action": "selectCommand", "direction": "prev" }, },
{ "command": { "action": "selectCommand", "direction": "next" }, },
]How you enable these marks in your shell varies from shell to shell. Below are tutorials for CMD, PowerShell and Zsh.
If you've never changed your PowerShell prompt before, you should check out about_Prompts first.
We'll need to edit your prompt to make sure we tell the Terminal about the CWD, and mark up the prompt with the appropriate marks. PowerShell also lets us include the error code from the previous command in the 133;D sequence, which will let the terminal automatically colorize the mark based if the command succeeded or failed.
Add the following to your PowerShell profile:
$Global:__LastHistoryId = -1
function Global:__Terminal-Get-LastExitCode {
if ($? -eq $True) {
return 0
}
$LastHistoryEntry = $(Get-History -Count 1)
$IsPowerShellError = $Error[0].InvocationInfo.HistoryId -eq $LastHistoryEntry.Id
if ($IsPowerShellError) {
return -1
}
return $LastExitCode
}
function prompt {
# First, emit a mark for the _end_ of the previous command.
$gle = $(__Terminal-Get-LastExitCode);
$LastHistoryEntry = $(Get-History -Count 1)
# Skip finishing the command if the first command has not yet started
if ($Global:__LastHistoryId -ne -1) {
if ($LastHistoryEntry.Id -eq $Global:__LastHistoryId) {
# Don't provide a command line or exit code if there was no history entry (eg. ctrl+c, enter on no command)
$out += "`e]133;D`a"
} else {
$out += "`e]133;D;$gle`a"
}
}
$loc = $($executionContext.SessionState.Path.CurrentLocation);
# Prompt started
$out += "`e]133;A$([char]07)";
# CWD
$out += "`e]9;9;`"$loc`"$([char]07)";
# (your prompt here)
$out += "PWSH $loc$('>' * ($nestedPromptLevel + 1)) ";
# Prompt ended, Command started
$out += "`e]133;B$([char]07)";
$Global:__LastHistoryId = $LastHistoryEntry.Id
return $out
}Using oh-my-posh? You'll want to slightly modify the above, to stash away the original prompt, then add it back in the middle of the shell integration escape sequences.
# initialize oh-my-posh at the top of your profile.ps1
oh-my-posh init pwsh --config "$env:POSH_THEMES_PATH\gruvbox.omp.json" | Invoke-Expression
# then stash away the prompt() that oh-my-posh sets
$Global:__OriginalPrompt = $function:Prompt
function Global:__Terminal-Get-LastExitCode {
if ($? -eq $True) { return 0 }
$LastHistoryEntry = $(Get-History -Count 1)
$IsPowerShellError = $Error[0].InvocationInfo.HistoryId -eq $LastHistoryEntry.Id
if ($IsPowerShellError) { return -1 }
return $LastExitCode
}
function prompt {
$gle = $(__Terminal-Get-LastExitCode);
$LastHistoryEntry = $(Get-History -Count 1)
if ($Global:__LastHistoryId -ne -1) {
if ($LastHistoryEntry.Id -eq $Global:__LastHistoryId) {
$out += "`e]133;D`a"
} else {
$out += "`e]133;D;$gle`a"
}
}
$loc = $($executionContext.SessionState.Path.CurrentLocation);
$out += "`e]133;A$([char]07)";
$out += "`e]9;9;`"$loc`"$([char]07)";
$out += $Global:__OriginalPrompt.Invoke(); # <-- This line adds the original prompt back
$out += "`e]133;B$([char]07)";
$Global:__LastHistoryId = $LastHistoryEntry.Id
return $out
}Command Prompt sources its prompt from the PROMPT environment variable. CMD.exe reads $e as the ESC character. Unfortunately, CMD.exe doesn't have a way to get the return code of the previous command in the prompt, so we're not able to provide success / error information in CMD prompts.
You can change the prompt for the current CMD.exe instance by running:
PROMPT $e]133;D$e\$e]133;A$e\$e]9;9;$P$e\$P$G$e]133;B$e\Or, you can set the variable from the commandline for all future sessions:
setx PROMPT $e]133;D$e\$e]133;A$e\$e]9;9;$P$e\$P$G$e]133;B$e\These examples assume your current PROMPT is just $P$G. You can instead choose to wrap your current prompt with something like:
PROMPT $e]133;D$e\$e]133;A$e\$e]9;9;$P$e\%PROMPT%$e]133;B$e\You may source the following script to an active shell with source or .
built-in bash commands or add it to the end of your ${HOME}/.bash_profile
(for login shells) or ${HOME}/.bashrc (for non-login shells) to enable
a complete shell integration in bash with versions greater or equal to
bash-4.4 (where the PS0 built-in variable was implemented
initially).
Complete shell integration means that every announced terminal feature works as
designed.
Note
It should be pointed out that if there are PROMPT_COMMAND, PS0, PS1 or
PS2 variables already assigned to any non-default
values that
may lead to unpredictable results. It would be better to test the script with
the "clean" shell first by executing env --ignore-environment bash --noprofile --norc and sourcing the described file as it was indicated
earlier.
# .bash_profile | .bashrc
function __set_ps1() {
local PS1_TMP="${__PS1_BASE}"
if [ ! -z "${__IS_WT}" ]; then
local __FTCS_CMD_FINISHED='\e]133;D;'"${1}"'\e\\'
PS1_TMP="\[${__FTCS_CMD_FINISHED}\]${__PS1_BASE}"
fi
printf '%s' "${PS1_TMP}"
}
function __prompt_command() {
# Must be first in the list otherwise the exit status will be overwritten.
local PS1_EXIT_STATUS=${?}
PS1="$(__set_ps1 ${PS1_EXIT_STATUS})"
}
# ---------------------------------------------------------------------------
# PROMPT (PS0..PS2).
# The given variable might be linked to a function detecting whether `bash`
# actually runs under `Microsoft Terminal` otherwise unexpected garbage might
# be displayed on the user screen.
__IS_WT='true'
printf -v __BASH_V '%d' ${BASH_VERSINFO[*]:0:2}
if [ ${__BASH_V} -ge 44 ]; then
__PS0_BASE=''
fi
# The following assignments reflect the default values.
__PS1_BASE='\s-\v\$ '
__PS2_BASE='> '
if [ ! -z "${__IS_WT}" ]; then
__FTCS_PROMPT='\e]133;A\e\\'
__FTCS_CMD_START='\e]133;B\e\\'
if [ ${__BASH_V} -ge 44 ]; then
__FTCS_CMD_EXECUTED='\e]133;C\e\\'
__PS0_BASE="\[${__FTCS_CMD_EXECUTED}\]"
fi
__PS1_BASE="\[${__FTCS_PROMPT}\]${__PS1_BASE}\[${__FTCS_CMD_START}\]"
# Required, otherwise the `PS2` prefix will split and corrupt a long
# command.
__PS2_BASE=''
fi
PROMPT_COMMAND=__prompt_command
if [ ${__BASH_V} -ge 44 ]; then
PS0="${__PS0_BASE}"
fi
# `PS1` is set with the `__prompt_command` function call.
PS2="${__PS2_BASE}"That wraps all assortment of bash prompt variables (PS0, PS1 and PS2)
with the necessary sequences to enable the complete shell integration.
In addition, ${HOME}/.inputrc might also need an adjustment to remove the
"editing mode notification" and "modified lines" signs:
# .inputrc
set mark-modified-lines Off
set show-mode-in-prompt OffThat is what it should look like if everything is done correctly:
$ env --ignore-environment bash --noprofile --norc
bash-5.2$ . /tmp/msft-terminal-bash.sh
bash-5.2$ echo "|${PS0}|"
|\[\e]133;C\e\\\]|
bash-5.2$ echo "|${PS1}|"
|\[\e]133;D;0\e\\\]\[\e]133;A\e\\\]\s-\v\$ \[\e]133;B\e\\\]|
bash-5.2$ echo "|${PS2}|"
||For the fish shell, you can enable shell integration by creating a new file, for example ~/.config/fish/conf.d/wt_integration.fish, and adding the following script.
This script leverages fish's event system to automatically send the necessary shell integration sequences. It is designed to be robust, only activating when in an interactive session within Windows Terminal (by checking for the $WT_SESSION variable). It also includes a specific function to correctly report the current working directory when running under WSL by using wslpath.
Add the following to your ~/.config/fish/conf.d/wt_integration.fish file:
# Only activate in an interactive session running inside Windows Terminal
if status --is-interactive; and set -q WT_SESSION
# Define the FTCS (Final Term Command Sequences) for shell integration
set -g __fish_wt_prompt_start (printf "\e]133;A\e\\")
set -g __fish_wt_prompt_end (printf "\e]133;B\e\\")
set -g __fish_wt_cmd_executed (printf "\e]133;C\e\\")
set -g __fish_wt_cmd_finished_pre (printf "\e]133;D;")
set -g __fish_wt_cmd_finished_post (printf "\e\\")
# Event handler: Fired before a command is executed.
# Sends the "Command Executed" sequence.
function __fish_wt_preexec --on-event fish_preexec
printf '%s' $__fish_wt_cmd_executed
end
# Event handler: Fired after a command has finished.
# Sends the "Command Finished" sequence with the command's exit status.
function __fish_wt_postexec --on-event fish_postexec
printf '%s%s%s' $__fish_wt_cmd_finished_pre $status $__fish_wt_cmd_finished_post
end
# Event handler: Fired before the prompt is displayed.
# Updates the terminal's current working directory (CWD).
# Uses wslpath to translate WSL paths to Windows paths for compatibility.
function __fish_wt_update_cwd --on-event fish_prompt
printf "\e]9;9;\"%s\"\e\\" (wslpath -w (pwd))
end
# Overrides the default fish_prompt function to wrap it with FTCS sequences.
# This example shows a simple prompt. See below for a Starship example.
function fish_prompt
# Send "Prompt Start" sequence
printf '%s' $__fish_wt_prompt_start
# --- YOUR PROMPT GENERATION LOGIC GOES HERE ---
# A basic example:
printf '%s@%s %s> ' (whoami) (hostname|cut -d . -f 1) (prompt_pwd)
# Send "Prompt End" (or "Command Start") sequence
printf '%s' $__fish_wt_prompt_end
end
endIf you are using Starship as your prompt, you will need to modify the fish_prompt function to wrap the starship command. This ensures Starship's output is correctly placed between the start and end prompt markers.
Replace the fish_prompt function from the script above with the following:
function fish_prompt
set -l last_status $status
# Get the number of background jobs
set -l job_count (count (jobs -p))
# Send "Prompt Start" sequence
printf '%s' $__fish_wt_prompt_start
# Render the Starship prompt and remove any trailing newlines
starship prompt --status=$last_status --jobs=$job_count | tr -d "\n"
# Send "Prompt End" (or "Command Start") sequence
printf '%s' $__fish_wt_prompt_end
endNote
Don't see your favorite shell here? If you figure it out, feel free to contribute a solution for your preferred shell!
This uses the scrollToMark actions as we have them defined above.
In this gif, we use the selectOutput action bound to ctrl+g to select the entire output of a command.

The following uses the experimental.rightClickContextMenu setting to enable a right-click context menu in the Terminal. With that and shell integration enabled, you can right-click on a command to select the entire command or its output.
With shell integration enabled, the Suggestions UI can be configured to also show your recent commands.
You can open this menu with the following action:
{
"command": { "action": "showSuggestions", "source": "recentCommands", "useCommandline": true },
},(For more info, see the Suggestions documentation)




