Pressing Ctrl+C in a terminal sends SIGINT to the foreground process group so a
runaway command can stop. In a script you handle that with trap: register a
command list for INT or SIGINT, and Bash runs that list when the signal
arrives. Inside the handler you can exit, run cleanup, or temporarily ignore the
interrupt with an empty trap.
Tested all the commands from this article on Ubuntu 25.04, kernel 6.14.0-37-generic, Bash 5.2.37.
Long-running loops are where this usually matters; see
bash while loop for structure, then wire trap
around them.
Ctrl+C, SIGINT, and the linux ctrl c signal
On typical Linux terminals, Ctrl+C generates SIGINT (signal 2). You can see the name in Bash’s signal list:
trap -l | head -1Output:
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAPSIGKILL and SIGSTOP cannot be caught or ignored—design choice of the kernel, not Bash.
Bash trap ctrl c (trap … INT)
Minimal registration (names INT and SIGINT are equivalent in trap):
trap 'echo interrupted; exit 0' INTTo exercise the handler without typing Ctrl+C in this article, you can deliver the same signal explicitly:
bash -c 'trap "echo caught" INT; kill -INT $$; echo after'Output:
caught
afterHere the handler does not exit, so the shell keeps going and prints after.
If your handler calls exit, lines after the signal will not run—often what you
want for a bash catch ctrl c cleanup path.
Calling a function keeps noisy logic out of the trap string:
bash -c 'f(){ echo in_func; }; trap f INT; kill -INT $$; echo after_func'Output:
in_func
after_funcCleanup on any exit (EXIT)
EXIT (signal number 0 in trap’s world) runs when the shell exits
normally, on exit, or on many interrupts—handy for rm’ing temp files once.
bash -c 'trap "echo on_exit" EXIT; echo body'Output:
body
on_exitCombine INT, TERM, and EXIT when you want one cleanup path for “user
pressed Ctrl+C”, “kill on the script”, and “finished successfully”.
Ignore Ctrl+C in a script (trap '' INT)
An empty command string tells Bash to ignore SIGINT for that shell until you reset it:
trap '' INTUse sparingly: operators expect Ctrl+C to stop whatever is in the foreground. Prefer short critical sections or document why interrupts are disabled.
Reset to default (trap - INT)
trap - INT removes the INT handler and restores the default disposition
(usually terminate the process).
Quoting: why trap '…' INT matters for bash catch ctrl c
trap records a string and evaluates it later. With double quotes, variables
expand when trap runs, not when the signal fires; single quotes defer
expansion until the trap executes.
bash -c 'x=1; trap "echo x is $x" INT; x=2; kill -INT $$'Output:
x is 1bash -c 'x=1; trap '\''echo x is $x'\'' INT; x=2; kill -INT $$'Output:
x is 2(The second example nests quotes so the inner $x is literal in the outer shell
until the trap fires.)
Subshells, children, and Git Bash Ctrl+C
A trap set inside ( … ) applies only to that subshell’s lifetime. A
background sleep is a different process: it does not “inherit” your trap
body; stopping sleep with Ctrl+C in the terminal sends SIGINT to the
foreground job’s process group, which may or may not include your script depending on
how jobs are set up.
Git Bash on Windows still runs Bash; trap / SIGINT behavior for scripts
matches the Bash manual closely enough that everything above applies—terminal and
pseudo-TTY details differ, not the basic trap '…' INT model.
Summary
Ctrl+C normally sends SIGINT. Register a handler with trap '…' INT (or
SIGINT), run cleanup or print a message, and call exit when you want the script to
stop. trap … EXIT runs when the shell exits for many reasons, including normal
completion. trap '' INT ignores interrupts until you reset with trap - INT. Use
single quotes in trap when variables must expand at signal time, not when trap is
parsed. SIGKILL and SIGSTOP cannot be trapped. Git Bash follows the same Bash
rules for scripts; differences usually show up in the terminal layer, not in trap
itself.
To factor cleanup helpers or option parsing into reusable units, Bash functions pair naturally with trap, but remember traps follow the usual subshell rules when you wrap code in ( … ).
Further reading:

