Non-Interactive Shell Behavior in CI Runners
A CI runner is not your terminal. Reliable pipeline scripts assume a non-interactive shell, explicit environment, controlled working directory, ephemeral local state, and success determined by process exit status.
Learning objectives
By the end of this lesson
- Explain interactive versus non-interactive CI shell behavior.
- Avoid dependencies on startup files and terminal prompts.
- Control working directory, pagers, and editors.
- Design for ephemeral runners and cancellation.
- Use exit status and logs as the job interface.
1. CI shells are not interactive terminals
A CI runner launches commands in a controlled non-interactive environment. There may be no terminal, no prompt, no user profile, no remembered working directory from your laptop, and no opportunity for someone to answer a question.
flowchart TD Y["pipeline definition"] --> R["CI runner"] R --> S["non-interactive shell"] S --> C["script / commands"] C --> E["exit status + logs + artifacts"]
Any command that may stop and wait for input is a production risk in CI.
2. Shell startup files may not run
printf 'shell=%s\n' "${SHELL:-unknown}"
printf 'bash_version=%s\n' "${BASH_VERSION:-not-bash}"
printf 'home=%s\n' "${HOME:-unset}"
printf 'path=%s\n' "$PATH"
Do not rely on aliases, functions, PATH additions, language-version
managers, or exports from your interactive ~/.bashrc.
Configure dependencies explicitly in the job.
3. Know which shell is actually executing the step
if [[ -z ${BASH_VERSION:-} ]]; then
printf 'this script requires Bash\n' >&2
exit 69
fi
printf 'running Bash %s\n' "$BASH_VERSION"
A pipeline may default to sh, PowerShell, cmd.exe, or
another shell depending on runner and platform. Bash-specific
scripts should use a Bash shebang or invoke Bash deliberately.
4. Commands that require a TTY often fail or hang
# Prefer non-interactive flags:
sudo -n true
# For tools with prompts, use their documented non-interactive mode.
some_cli --non-interactive
Interactive password prompts, confirmation menus, pagers, editors, and credential dialogs are unsuitable unless the CI platform explicitly provides a supported mechanism.
5. Disable pagers and editors
export PAGER=cat
export GIT_PAGER=cat
export GIT_EDITOR=:
export SYSTEMD_PAGER=cat
Tools that open a pager or editor can block unattended jobs. Prefer
tool-specific flags such as --no-pager where available.
6. Working directory is part of the contract
script_dir=$(
cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &&
pwd -P
) || exit 1
repo_root=$(
cd -- "$script_dir/../.." &&
pwd -P
) || exit 1
cd -- "$repo_root" || exit 1
Do not assume the job starts in a repository root unless the pipeline explicitly guarantees it.
7. Runner filesystems may be ephemeral
Many CI jobs execute on short-lived machines or containers. Files created during one job may disappear when that job ends unless you explicitly persist them as artifacts, cache entries, or platform-supported state.
Treat every job as a fresh process boundary unless the CI system explicitly documents shared state.
8. Runners can cancel jobs
cleanup() {
local status=$?
rm -rf -- "${tmpdir:-}"
exit "$status"
}
trap cleanup EXIT TERM INT
Cancellation may send signals before the runner forcefully terminates the job. Cleanup should be useful but never assumed to be guaranteed.
9. stdout and stderr become the operator interface
printf 'artifact_id=%s\n' "$artifact_id"
printf 'level=INFO event=build_started\n' >&2
CI logs are often the only forensic record after an ephemeral runner disappears. Keep result data and diagnostics intentionally separated.
10. Exit status is how a job communicates success
if run_tests; then
printf 'tests=passed\n'
else
status=$?
printf 'tests=failed status=%d\n' "$status" >&2
exit "$status"
fi
A command that prints an error but exits 0 can make the whole job appear successful. Conversely, a command whose non-zero status is expected must be handled explicitly.
11. Hands-on lab: simulate a CI-like shell
mkdir -p "$HOME/devops-academy/bash/chapter16/lesson01"
cd "$HOME/devops-academy/bash/chapter16/lesson01"
cat > ci-env-check.sh <<'EOF'
#!/usr/bin/env bash
set -u
set -o pipefail
[[ -n ${BASH_VERSION:-} ]] || {
printf 'Bash required\n' >&2
exit 69
}
export PAGER=cat
export GIT_PAGER=cat
export GIT_EDITOR=:
printf 'interactive=%s\n' \
"$([[ $- == *i* ]] && printf yes || printf no)"
printf 'bash=%s\n' "$BASH_VERSION"
printf 'pwd=%s\n' "$PWD"
printf 'home=%s\n' "${HOME:-unset}"
printf 'ci=%s\n' "${CI:-false}"
if [[ -t 0 ]]; then
printf 'stdin_tty=yes\n'
else
printf 'stdin_tty=no\n'
fi
EOF
chmod u+x ci-env-check.sh
env -i \
HOME="$HOME" \
PATH="$PATH" \
CI=true \
bash --noprofile --norc ./ci-env-check.sh </dev/null
Verification checklist
12. Knowledge check
Question 1. Why should CI scripts avoid interactive commands?
Question 2. Can a Bash script assume
~/.bashrc ran in CI?
Question 3. What makes a job fail at the pipeline level?
Question 4. Why should CI jobs treat local files as ephemeral?
13. Summary
CI shells are non-interactive, frequently ephemeral, and controlled by exit status. Make the shell, working directory, dependencies, pager/editor behavior, cleanup, and state persistence explicit instead of inheriting assumptions from an interactive workstation.
14. Further reading
- GNU Bash Reference Manual — invocation and startup files.
- GNU Bash Reference Manual — interactive versus non-interactive shells.
- POSIX shell execution and exit status concepts.
- CI runner documentation for the platform used by your project.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.