Shell Configuration
The shell option controls whether a command runs through a shell, and which one. Every value interpolated into a $`...` template is quoted for that shell automatically.
The shell option
shell is boolean | string:
true(default) — delegates to Node's built-in shell handling:/bin/sh -c "command"on POSIX,cmd.exeon Windows. This is what runs whenshellis left unset.- a string — the path or bare name of a single executable, invoked as
<shell> -c "command". It must be just the executable:'/bin/bash -i'is not a valid path and spawning it fails withENOENT— there is no separate field for extra flags. false— no shell.commandis executed asargv[0]withargsas its arguments; pipes, redirection and$VARexpansion are not available.
await $`ls -la`; // shell: true (default)
await $.shell('/bin/bash')`echo $BASH_VERSION`;
await $.shell('/bin/zsh')`echo $ZSH_VERSION`;
// Skip the shell. This needs args as a separate array, which the template
// literal form can't produce — it always collapses to one command string.
await $.exec('node', { args: ['--version'], shell: false });
.shell(...) exists both on $ (persists on the returned engine) and on a single pending command — $`cmd`.shell('/bin/zsh'), applied before it starts.
A string shell is always invoked with -c, which is why this works for shells that accept that flag for a command string — bash, zsh, sh, dash and fish all do. On Windows, leave cmd.exe as the default (shell: true): an explicit shell: 'cmd.exe' would still be invoked with -c, which cmd.exe doesn't understand — it needs /c, so it can't be selected as a string shell this way.
Configuring a target's shell
# .xec/config.yaml
targets:
local:
type: local
shell: /bin/bash
There's no shellArgs field — shell is the whole story, and it takes exactly one executable path.
Startup files
A string shell runs non-interactively via -c, so interactive/login startup files are not sourced automatically:
| Shell | Sourced by -c |
|---|---|
| bash | $BASH_ENV, if set — not .bashrc or .bash_profile |
| zsh | $ZDOTDIR/.zshenv — not .zshrc |
| sh | nothing |
Source what you need explicitly:
await $.shell('/bin/bash')`source ~/.bashrc && my-alias`;
Quoting and escaping
Values interpolated into a $`...` template are quoted for the shell that will run them. The dialect ('posix', 'cmd' or 'powershell') is resolved from whatever .shell(...) is currently set — not from the host OS:
const userInput = "'; rm -rf /";
await $`echo ${userInput}`; // one literal argument, not executed
await $.shell('pwsh')`echo ${userInput}`; // quoted for PowerShell, even on Linux
Quoting only prevents value injection. It can't stop option injection — a value of -rf is a well-formed argument in any dialect. Use an explicit -- separator at the call site if a value might be attacker-controlled and read as a flag.
To quote a value you're assembling into a command string yourself — for $.exec(), a generated script, a log line — rather than through the auto-escaping template:
import { quoteForShell } from '@xec-sh/core';
const safe = quoteForShell(userInput, 'posix');
await $.exec('echo ' + safe);
Portability
sh is POSIX-only — no arrays, no [[ ]], no brace expansion:
// Fails under /bin/sh
await $.shell('/bin/sh')`[[ -f file ]] && echo exists`;
// Works under any POSIX shell
await $`[ -f file ] && echo exists`;
await $.shell('/bin/bash')`[[ -f file ]] && echo exists`;
None of those reach Windows, whose default shell is cmd.exe — it has
neither [ nor &&. See
Windows and cross-platform scripts for what carries
across operating systems and what does not.
Related Documentation
- Local Overview - local target fundamentals
- Windows and cross-platform scripts - what behaves identically on every OS
- Troubleshooting - common shell issues