Taming the Interrupts
Ctrl-C must kill your command and never your shell.
What you're wiring up
Ctrl-C does not send a character your shell reads. The terminal driver sees the key and raises SIGINT, an asynchronous tap on the shoulder delivered not to one process but to an entire foreground process group. Your job is to arrange the groups so that the tap lands on the command you are running and never on the shell running it.
That means each command gets its own process group, and the shell lends the terminal to that group with tcsetpgrp for the duration of the command, then takes it back. Meanwhile the shell ignores SIGINT and SIGQUIT for itself, and every child restores the default dispositions before exec, because ignored signals are inherited across exec and would silently make your commands uninterruptible.
The other half is bookkeeping. When a child dies the kernel keeps its corpse, a zombie, until the parent collects the status. A long-lived process that forks constantly must reap or rot. And a child that dies from a signal reports differently: WIFSIGNALED, with $? set to 128 plus the signal number, which is why 130 means killed by Ctrl-C and why scripts test whether $? is greater than 128.
From here the harness talks to your shell through a PTY, a pretend terminal, because signal routing is terminal behavior and simply cannot be tested through plain pipes.
Assembly steps
hint
Both sides call it because either may be scheduled first. The man page recommends exactly this.
hint
The shell must ignore SIGTTOU around tcsetpgrp, or the act of reclaiming the terminal from the background stops the shell itself.
hint
sigaction with SIG_IGN in the shell. Ignored dispositions survive exec, so resetting in the child is mandatory, not tidiness.
hint
130 is SIGINT. Check it with the WTERMSIG macro rather than decoding the raw status yourself.
Go deeper (after it passes)
Async signal handlers are the C version of the shared-state hazards in Clean Code M10 — Clean Concurrency, and the flag-only-act-later rule is its discipline made concrete. Read signal-safety(7) once; the list of functions you may legally call in a handler is shorter than you expect.