The typo that cost you an hour
You are building a command-line tool. Every time you want to run the library tests with output captured, you type cargo test --lib -- --nocapture. You do this twenty times a day. On the twenty-first attempt, your fingers slip. You type --nocaptuer. The test runner ignores the typo, runs the tests silently, and swallows the panic output. You miss a critical assertion failure. You spend an hour debugging logic that was actually correct, only to realize the test output was hidden by a flag you forgot to type.
This is where Cargo aliases save your sanity. Aliases let you define short, memorable names for long command sequences. You type cargo tl, and Cargo expands it to cargo test --lib -- --nocapture. No typos. No muscle memory fatigue. The shortcut lives in your project config, so every developer on the team gets it automatically.
Aliases are text substitution, not magic
Cargo aliases are a simple expansion mechanism. You define a mapping in a TOML configuration file. When you run cargo <alias>, Cargo looks up the alias, replaces the name with the defined subcommand and arguments, and executes the result.
Think of it like a macro in a text editor. You type fn, and the editor expands it to function. Cargo does the same thing. You type cargo qa, and Cargo expands it to cargo clippy --all-targets. The expansion happens before Cargo parses the arguments. This means you can still pass extra arguments to the expanded command. cargo qa --fix becomes cargo clippy --all-targets --fix.
Custom commands work differently. If you install a binary named cargo-audit, you can run cargo audit. Cargo searches your system PATH for a binary starting with cargo-. If it finds one, it runs that binary directly. Custom commands are external tools. Aliases are internal shortcuts. Both let you type less, but they solve different problems.
Minimal example: project-specific shortcuts
Create a file named .cargo/config.toml in the root of your project. Add an [alias] section and map names to subcommands.
# .cargo/config.toml
[alias]
# Run only library tests with output visible
tl = "test --lib -- --nocapture"
# Build in release mode without typing the flag
b = "build --release"
# Check all targets including tests and benchmarks
ca = "check --all-targets"
Run cargo tl to test the library. Run cargo b to build. Run cargo ca to check everything.
$ cargo tl
Compiling my_crate v0.1.0
Running unittests src/lib.rs
test result: ok. 5 passed; 0 failed
The alias tl expands to test --lib -- --nocapture. Cargo runs cargo test --lib -- --nocapture. The -- separates Cargo arguments from test harness arguments. The alias preserves this boundary.
Convention aside: avoid aliasing built-in commands like test or build. If you write test = "test --lib", you replace the test command entirely. Running cargo test --bin my_app will fail because the alias forces --lib. Use distinct names like tl, test-lib, or tlib. Keep the built-ins available for edge cases.
How expansion handles arguments
Aliases expand before argument parsing. This behavior is powerful and predictable. When you run cargo tl my_module, Cargo expands tl to test --lib -- --nocapture. The result is cargo test --lib -- --nocapture my_module. The argument my_module appends to the end. The test harness receives it and filters tests by name.
This works for flags too. cargo tl -- --test-threads=1 becomes cargo test --lib -- --nocapture -- --test-threads=1. The test harness sees both flags.
You can also alias commands that take arguments. cargo run-dev is not a real subcommand, but you can create an alias for it.
[alias]
# Run the dev binary with default arguments
run-dev = "run --bin dev-server"
Now cargo run-dev runs the dev-server binary. If you need to pass arguments to the binary, use --. cargo run-dev -- --port 8080 expands to cargo run --bin dev-server -- --port 8080. The arguments pass through to your program.
Aliases are strictly text replacement. They do not support shell features. You cannot use &&, ||, pipes, or variable expansion. The alias value must be a valid Cargo subcommand followed by arguments. If you try to chain commands, Cargo will fail.
[alias]
# This will NOT work. Cargo sees "test" as the subcommand.
# "&&" is treated as an argument to "test", which rejects it.
deploy = "test && build && upload"
Running cargo deploy produces an error. Cargo expands the alias to test && build && upload. It treats test as the subcommand. It passes && as an argument to test. The test subcommand does not recognize &&. You get an error like error: unexpected argument '--' found.
Aliases are not shell scripts. If you need complex logic, use a custom command or a shell function.
Custom commands: the ecosystem extension point
Custom commands let you extend Cargo with external tools. The Rust ecosystem relies on this mechanism. Tools like cargo-audit, cargo-outdated, cargo-expand, and cargo-udeps are all custom commands.
Install a custom command using cargo install. The binary must be named cargo-<command>.
$ cargo install cargo-audit
Once installed, you can run cargo audit. Cargo finds the cargo-audit binary in your PATH and executes it. The command receives any arguments you pass. cargo audit --ignore RUSTSEC-2021-0001 passes the flag to the tool.
Custom commands are independent of your project. They live in your global toolchain directory. You can use them in any project. They are ideal for cross-cutting concerns like security auditing, dependency analysis, or code generation.
You can also write your own custom command. Create a binary crate named cargo-mytool. Implement the logic in main. Install it with cargo install. Now cargo mytool runs your tool. This is how you build reusable CLI utilities that integrate seamlessly with Cargo workflows.
Convention aside: custom commands should follow Cargo's output conventions. Print errors to stderr. Use color when stdout is a terminal. Exit with code 0 on success and non-zero on failure. This keeps your tool consistent with the rest of the ecosystem.
Pitfalls and gotchas
Aliases and custom commands are simple, but a few traps catch developers off guard.
Overriding built-ins breaks flexibility. If you alias test = "test --lib", you lose the ability to run cargo test --bin. The alias replaces the command. You cannot recover the original behavior without removing the alias. Always use unique names for aliases. Reserve built-in names for their default behavior.
Shell syntax fails silently or loudly. Aliases do not support &&, ||, |, or $VAR. If you write alias = "cmd1 && cmd2", Cargo treats cmd1 as the subcommand and && as an argument. The command fails with a parse error. If you need to run multiple steps, create a custom command that calls them programmatically, or use a shell script.
Scope confusion causes surprises. .cargo/config.toml applies to the current project. ~/.cargo/config.toml applies to all projects for your user. If you define a global alias like t = "test", it affects every project you open. This can conflict with project-specific aliases or expectations. Use project config for project-specific shortcuts. Use global config for universal preferences like build.target or net.git-fetch-with-cli.
Naming collisions create ambiguity. If you have a custom command cargo-fmt and an alias fmt = "run --bin fmt", Cargo might behave unexpectedly. The resolution order can depend on the Cargo version. Avoid naming aliases the same as installed custom commands. Check your PATH for cargo-* binaries before defining aliases that shadow them.
Aliases do not support environment variables. You cannot write alias = "run --env KEY=VALUE". The run subcommand supports --env, but the alias expansion happens before argument parsing. You can alias run-env = "run --env KEY=VALUE", but you cannot inject dynamic values. For dynamic environments, use shell functions or cargo run --env.
Trust the expansion model. Aliases are text substitution. If you need anything beyond that, you are using the wrong tool.
Decision: when to use aliases, custom commands, or shell
Choose the right mechanism for your workflow. Each has a specific role.
Use Cargo aliases when you need a shorter name for a standard Cargo subcommand with fixed flags. Use Cargo aliases when you want project-specific shortcuts that live in version control with your code. Use Cargo aliases when you need to preserve argument passing to the underlying subcommand.
Use custom commands when you need to run arbitrary logic that is not part of Cargo. Use custom commands when you want to distribute a tool that integrates with Cargo workflows across multiple projects. Use custom commands when you need to chain multiple steps, access the file system, or perform network requests.
Use shell aliases or functions when you need full shell power like pipes, conditionals, or environment variable manipulation. Use shell aliases when the shortcut is personal and should not be shared with the team. Use shell functions when you need dynamic behavior based on arguments or state.
Keep aliases in the repo so your teammates get them too. Treat the alias file as part of your project documentation. If a shortcut is essential for development, commit it.