ctrlc: a small Rust crate for handling Ctrl-C without the boilerplate
Easy Ctrl-C handler for Rust projects
At a glance
- What is it?
- The ctrlc crate wraps the Ctrl-C signal in a handler you register from a normal function, with an optional termination feature that also covers SIGTERM and SIGHUP. It fits any Rust binary that needs a clean shutdown path, and it is deliberately small enough that writing the handler yourself is a reasonable alternative.
- Who is it for?
- ctrlc fits a Rust program that needs one clean shutdown path on Ctrl-C, especially on Windows where the standard library offers no equivalent, and it is cheap enough to add to a small binary. It is the wrong choice for a service built on an async runtime, since the crate's own README points to tokio::signal for that, and it is unnecessary for a program with no state to flush.
- Can I use it commercially?
- Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
- Is it still maintained?
- Yes. The repository last received commits 93 days ago.
- What is it written in?
- Mainly Rust, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
One function call replaces the signal handler plumbing
The ctrlc crate is described in the README as a simple, easy to use wrapper around the Ctrl-C signal. The whole public surface is one function.
You add it to your dependencies:
[dependencies]
ctrlc = "3.5"and then register a handler from ordinary Rust code:
use std::sync::mpsc::channel;
use ctrlc;
fn main() {
let (tx, rx) = channel();
ctrlc::set_handler(move || tx.send(()).expect("Could not send signal on channel."))
.expect("Error setting Ctrl-C handler");
println!("Waiting for Ctrl-C...");
rx.recv().expect("Could not receive from channel.");
println!("Got it! Exiting...");
}The example shows the shape of the intended usage. The handler is a closure, and because it has to be safe to call from a signal context it must not do much. The example sends a unit value down a channel, and the main thread blocks on receiving from it, which is the pattern the crate is nudging you towards: the handler signals, your main thread decides what shutting down means.
That separation is the point of the design. A handler that tried to close a file, flush a buffer or talk to a network socket would be doing work in a context where that is not safe. Moving the work to the main thread is a small amount of ceremony for a real guarantee.
Both set_handler calls return a Result, and the example treats failure as an error, which is the right instinct. A program that silently has no Ctrl-C handler will look like it is ignoring the keyboard.
The termination feature is a compile-time decision, not a runtime one
The README has a short section on handling SIGTERM and SIGHUP. Adding CtrlC to Cargo.toml using the termination feature means CtrlC will handle SIGINT, SIGTERM and SIGHUP.
That is one line of configuration in Cargo.toml, and it is worth pausing on what it implies. These signals are not the same thing. SIGINT is what a terminal sends when you press Ctrl-C, and it is what you get when someone runs kill on a process. SIGTERM is the polite request to terminate, sent by init systems and container orchestrators when they shut a service down. SIGHUP traditionally means the terminal hung up, and on a daemon it is often used to mean reload the configuration.
The consequence of the feature flag is that the set of signals you handle is fixed at compile time, and the crate does not appear to expose a runtime way to choose which ones you want. A program that needs to distinguish a configuration reload from a shutdown request cannot do that with this crate, because by the time the handler fires, the only thing your closure receives is nothing at all. You get a callback, not a reason.
The practical advice follows from the deployment reality. A CLI tool that a user runs in a terminal needs SIGINT, and nothing else. A service that runs under a process supervisor needs SIGTERM, because that is what the supervisor sends, and a program that only handles SIGINT will be killed ungracefully at shutdown. A daemon that reloads configuration on SIGHUP needs a third behaviour that this crate cannot express.
For the second case, the termination feature is exactly right. For the third, you need something with more control, and the README points you at the alternatives for that.
Platform dependencies explain what the crate does under the hood
The Cargo.toml shows how the crate handles the differences between operating systems, and this is the part that explains its design.
On Unix, the dependency is nix with the signal feature, version 0.31, and default features turned off. On Apple targets there is an additional dependency on dispatch2 at 0.3, which is the Rust interface to Apple's Grand Central Dispatch. That is there because macOS does not deliver signals the way Linux does, and handling them needs a different mechanism.
On Windows, the dependency is windows-sys at 0.61 with a set of features covering the Windows Foundation, System Threading, System Security and System Console APIs. The dev-dependency for Windows adds file system features, which is there so the tests can check behaviour that needs file operations.
The picture that emerges is that the crate hides a genuine platform split from you. On Unix the signal handling is conventional, on macOS it goes through dispatch, and on Windows the console control handler API is a different mechanism entirely with different rules about which thread receives the event.
That Windows path is the main reason to use this crate rather than the standard library. The Rust standard library has no signal handling API, and on Unix you can get by with the nix crate directly. On Windows there is no equivalent in the ecosystem that is as small, and the console control handler model is different enough that hand-rolling it is a real cost.
The features table has exactly one entry, termination, which is an empty array. There are no other optional behaviours, which is a sign of a crate with a deliberately narrow surface.
The package also carries a rust-version of 1.69.0 and edition 2021, and the version in the manifest is 3.5.2, with 3.5.1 published on 2025-10-28 and 3.5.0 on 2025-09-09.
The test setup spawns real processes and real signals
The Cargo.toml configures the test targets by hand, with harness set to false and explicit paths. There is a main target at tests/main/mod.rs and a separate one for issue_97 at tests/main/issue_97.rs. Turning off the default harness means the tests are driven by a custom runner rather than by libtest, which is what you need when the code under test spawns child processes and sends them signals.
The dev-dependency on signal-hook at 0.3 gives a second signal implementation inside the test suite, which is a sensible way to check that the crate's behaviour agrees with an established alternative rather than only with itself.
The examples directory is small and purposeful. There is readme_example.rs, which is the code from the README, and issue_46_example.rs, which reproduces a reported problem. Keeping a regression example per issue is a habit worth copying, because it means the repository documents not just what the crate does but how it broke.
The README tells you how to run the example yourself with cargo build --examples followed by the built binary path, target/debug/examples/readme_example. That is a small detail, but it is the difference between an example you can read and one you can run, and for a crate whose entire purpose is asynchronous signal delivery, running it matters.
The crate is published on crates.io as ctrlc, and the documentation is hosted at detegr.github.io/doc/ctrlc.
Where ctrlc is the wrong tool
The first wrong-tool case is an async runtime, and the crate itself points at the answer. The README has a section on similar crates and says there are alternatives that give you more control over the different signals and add async support, naming signal-hook and tokio::signal.
If your program is built on tokio, using ctrlc means a blocking channel receive in a runtime context, or a thread that blocks while the runtime does the work. tokio::signal integrates the signal stream into the runtime and lets you select on it alongside everything else, which is the natural shape in an async program. The ctrlc README names it directly, so choosing wrongly here is a documentation failure, not a design failure.
The second case is a program with nothing to clean up. If your binary holds no open file, no buffer, no connection and no temporary state, the default behaviour of dying on Ctrl-C is already correct, and a dependency is pure overhead. That said, this crate is small, so the cost is low.
The third is a daemon with several distinct signals to handle. Reload on SIGHUP, drain on SIGTERM, abort on SIGINT, and the crate gives you one callback with no argument. You need something where the handler receives the signal number, and signal-hook is listed in the README as the alternative that gives more control over the different signals.
The fourth is a program that runs under a supervisor which already sends what it needs. Containers and systemd both send SIGTERM, and the termination feature covers it. But if your supervisor expects the process to exit with a particular code, or expects a second signal to mean abort immediately, the crate's uniform callback cannot express that distinction without you tracking state yourself.
The fifth is a codebase that has already standardised on another signal library. Adding a second one means two signal paths in one binary, and the two will not agree on what to do about a signal arriving twice.
Maintenance signal: a passively maintained badge and a July push
The Cargo.toml carries a badges section, and the entry is explicit: maintenance = { status = "passively-maintained" }. That is the crate author's own declaration about the project's state, and it should be read as written. It is not an abandoned repository, but it is not one where features are being planned either.
The dates are consistent with that. The last push to the repository was on 2026-07-22, and the most recent published releases are 3.5.1 on 2025-10-28 and 3.5.0 on 2025-09-09, with 3.4.7 before that in May 2025. The manifest version is 3.5.2, which is ahead of the last published tag, so the development branch has work that has not been cut as a release.
For a crate of this size, that is a perfectly healthy situation. The API is a single function, the platform dependencies are pinned, the test suite drives real processes, and the code is small enough to read in full. The risk of a stalled dependency here is small, and the risk of a stalled upstream signal crate is one level removed.
The licensing is the part that is worth checking properly. The README says the crate is licensed under either of the Apache License, Version 2.0, referring to the LICENSE-APACHE file, or the MIT license, referring to LICENSE-MIT, at your option. The Cargo.toml records the same choice as license = "MIT/Apache-2.0", and both licence files are at the repository root. A dual licence of this shape is the standard Rust convention and gives you a choice rather than a restriction.
The contribution terms are also stated, and they are worth a line: unless you explicitly state otherwise, any contribution intentionally submitted for inclusion is dual licensed as above, without any additional terms or conditions. That keeps the licensing of the crate uniform no matter who contributes, which is exactly what you want in a dependency you will link into a binary.
Editorial conclusion
ctrlc fits a Rust program that needs one clean shutdown path on Ctrl-C, especially on Windows where the standard library offers no equivalent, and it is cheap enough to add to a small binary. It is the wrong choice for a service built on an async runtime, since the crate's own README points to tokio::signal for that, and it is unnecessary for a program with no state to flush. Before adopting it, decide whether you need SIGTERM as well, because that is a feature flag rather than a runtime option, and note that the crate's own manifest marks it as passively maintained, with the last push on 2026-07-22.
Frequently asked questions
How do I add ctrlc to a Rust project?
Add ctrlc = "3.5" under [dependencies] in Cargo.toml, then call ctrlc::set_handler with a closure from your main function.
Does ctrlc handle SIGTERM and SIGHUP?
Yes, but only if you add the termination feature to the ctrlc dependency, in which case the crate handles SIGINT, SIGTERM and SIGHUP.
What licence is the ctrlc crate released under?
It is licensed under either the Apache License, Version 2.0 or the MIT license at your option, with the LICENSE-APACHE and LICENSE-MIT files at the repository root.
What should I use instead of ctrlc in an async Rust program?
The README names signal-hook and tokio::signal as alternatives that give more control over the different signals and add async support.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/detegr-rust-ctrlc)Was this useful?