A tool that ensures source code files have copyright license headers by scanning directory patterns recursively.
edlicense modifies source files in place and avoids adding a license header to any file that already has one. It follows the Unix philosophy of tooling where possible and is designed with modern Rust best practices for high-performance CLI tools.
edlicense is built for large polyglot monorepos (100k+ files) that lack centralized build systems like Bazel. Our guiding principles:
- Best-effort, not perfect: We use fast heuristics adopted from addlicense, like checking for the word "copyright" in the first N characters rather than matching exact license text. This trades precision for speed.
- Human review is the safety net: We assume every important file goes through code review where a human can catch formatting oddities. The goal is to get licenses on files reliably, not to handle every edge case perfectly.
- Heavy use of
.licenseignore: We expect you to liberally ignore files you don't care about or shouldn't add your license to (vendored code, generated files, assets, etc.). - Git-first: While
edlicenseworks without a git repository, we're fundamentally a git-first tool. Features like--git-only(default in git repos) and--ratchetreflect this.
If you need byte-perfect license validation or work in a tightly controlled build environment, a build-system-integrated solution may serve you better. If you need something fast and pragmatic that scales to massive repos, edlicense is for you.
edlicense is inspired by Google's addlicense tool but addresses several limitations:
| Feature | edlicense | addlicense |
|---|---|---|
| Implementation | Rust | Go |
| CLI interface | Modern long options (--option-name) | Short flags (-flag) |
| Default behavior | Dry run mode (non-destructive) | Modify mode |
| Automatic year updates | ✅ Updates copyright years automatically | ❌ No support for updating files w/ old years |
| Ratchet mode | ✅ Process only files changed since a git reference | ❌ Not available |
| Git integration | ✅ Option to only process git-tracked files | ❌ Not available |
Key advantages of edlicense:
- Safety First: Defaults to dry run mode, preventing accidental file modifications
- Git Integration:
- Ratchet mode for CI/CD pipelines to process only changed files
- Option to only process git-tracked files (default when in a git repository)
- Automatic Updates: Intelligently updates copyright years without manual intervention
Performance is an explicit goal for edlicense. We benchmark regularly against addlicense and work to identify optimization opportunities.
In our synthetic benchmarks processing thousands of files:
| Scenario | edlicense vs addlicense |
|---|---|
| Small files (1KB) | Comparable performance |
| Medium files (10KB) | ~1.5x faster |
| Large files (100KB) | ~2-4x faster |
Both tools are fast enough for typical CI usage. Where edlicense tends to pull ahead is with larger codebases and larger source files, where I/O efficiency becomes more important.
Benchmark methodology and raw results are available in benchmarks/. Performance characteristics vary by workload, file system, and hardware. Run your own benchmarks if performance is critical for your use case.
- Recursively scan directories and add license headers to source files
- Automatic detection of file types and appropriate comment formatting
- Dry run mode to verify license headers without modifying files (default behavior)
- Ignore patterns to exclude specific files or directories (via CLI or
.licenseignorefiles) - Support for
.licenseignorefiles with gitignore-style pattern matching - Global ignore file support via
GLOBAL_LICENSE_IGNOREenvironment variable - Automatic year reference updates - automatically updates copyright year references when the year changes (e.g.,
(c) 2024→(c) 2025) - Ratchet mode - only check and format files that have changed relative to a git reference (e.g.,
origin/main) - Git repository integration - option to only process files tracked by git (default when in a git repository)
cargo install edlicensegit clone https://github.com/eddieland/edlicense.git
cd edlicense
cargo install --path .edlicense is available as a Docker image, making it easy to run without installing Rust or any dependencies.
Pre-built Docker images are automatically published to GitHub Container Registry when code is pushed to the main branch.
# Pull the latest production image
docker pull ghcr.io/eddieland/edlicense:latest
# Pull the latest distroless image
docker pull ghcr.io/eddieland/edlicense:distroless-latest
# Run using the production image
docker run --rm -v "$(pwd):/workspace" -w /workspace ghcr.io/eddieland/edlicense:latest src/
# Run using the distroless image
docker run --rm -v "$(pwd):/workspace" -w /workspace ghcr.io/eddieland/edlicense:distroless-latest src/
# Run in modify mode
docker run --rm -v "$(pwd):/workspace" -w /workspace ghcr.io/eddieland/edlicense:latest --modify src/Available image tags:
ghcr.io/eddieland/edlicense:latest- Standard production image (latest version)ghcr.io/eddieland/edlicense:distroless-latest- Distroless image (latest version)ghcr.io/eddieland/edlicense:<commit-hash>- Production image at a specific commitghcr.io/eddieland/edlicense:distroless-<commit-hash>- Distroless image at a specific commit
The project uses a single Dockerfile that can build production, distroless, and debug images:
# Build the lightweight production image (Debian-based)
make docker-build
# Build the minimal distroless image
make docker-build-distroless
# Build the debug/development image
make docker-build-debug
# Build all images
make docker-build-all# Run edlicense on files in the current directory (dry run mode)
docker run --rm -v "$(pwd):/workspace" -w /workspace edlicense:latest src/
# Using the make target (equivalent to above)
make docker-run ARGS="src/"
# Run with the distroless image (smaller and more secure)
make docker-run-distroless ARGS="src/"
# Run edlicense in modify mode
docker run --rm -v "$(pwd):/workspace" -w /workspace edlicense:latest --modify src/
# Run with the debug image for development purposes
make docker-run-debug ARGS="cargo nextest run"The Docker setup provides three image tags from the same Dockerfile:
-
Lightweight image (
edlicense:latest): A Debian-based image containing only the compiled binary and minimal dependencies, optimized for CI/CD pipelines and general production use. -
Distroless image (
edlicense:distroless): An ultra-minimal image based on Google's distroless container, containing only the compiled binary and essential libraries with no shell or package manager. This provides the smallest possible attack surface and image size, ideal for security-sensitive deployments. -
Debug image (
edlicense:debug): A development image containing the full Rust toolchain, source code, and development tools, useful for debugging and development.
For advanced Docker usage, including building downstream images and handling file permissions, see Docker Usage Examples.
edlicense [OPTIONS] <PATTERNS>...
<PATTERNS>...- File or directory patterns to process. Directories are processed recursively.
--dry-run Dry run mode: only check for license headers without modifying files (default)
--modify Modify mode: add or update license headers in files
--show-diff Show diff of changes in dry run mode
--save-diff <FILE> Save diff of changes to a file in dry run mode
--license-file <LICENSE_FILE> Custom license file to use
--ignore <IGNORE>... File patterns to ignore (supports glob patterns)
--year <YEAR> Copyright year [default: current year]
--verbose Verbose logging
--ratchet <REFERENCE> Ratchet mode: only check and format files that have changed relative to a git reference
--preserve-years Preserve existing years in license headers
--global-ignore-file <FILE> Path to a global license ignore file (overrides GLOBAL_LICENSE_IGNORE environment variable)
--git-only Only consider files in the current git repository (default when in a git repository)
--help Print help
--version Print version
Note: --dry-run and --modify are mutually exclusive options. If neither is specified, dry run mode is used by default.
edlicense supports an optional TOML configuration file (.edlicense.toml) for customizing comment styles and extension filtering. The config file is discovered in this order:
- Path specified via
--config <FILE> - Path specified via the
EDLICENSE_CONFIGenvironment variable .edlicense.tomlin the workspace root
Use --no-config to skip config file discovery entirely.
The configuration file has three sections, all optional:
Override or add comment styles for file extensions. Keys are extensions without the leading dot.
Each entry can be either a full definition or an alias to a built-in extension's style:
[comment-styles]
# Full definition: line-style comments (only middle is required)
acme = { middle = "## " }
# Full definition: block-style comments
xyz = { top = "/*", middle = " * ", bottom = " */" }
# Alias: reuse a built-in extension's style
cjs = "js"
mts = "ts"Fields:
| Field | Required | Default | Description |
|---|---|---|---|
top |
No | "" |
Opening marker for block comments (e.g. /*) |
middle |
Yes | — | Line prefix (e.g. // , # , *) |
bottom |
No | "" |
Closing marker for block comments (e.g. */) |
Aliases reference a built-in extension by name (e.g. "js", "py", "rs"). If the referenced extension has no built-in style, the config will fail to load with an error.
Map specific filenames or glob patterns to comment styles. This is useful for files without extensions (e.g. Makefile, Dockerfile). Supports the same full definition and alias syntax as [comment-styles]:
[filenames]
"justfile" = { middle = "# " }
"*.cmake.in" = { middle = "# " }
# Or use an alias
"Vagrantfile" = "rb"Control which file extensions are processed. If include is specified, only those extensions are processed and exclude is ignored. If only exclude is specified, those extensions are skipped.
[extensions]
# Only process these extensions (all others are ignored)
include = ["rs", "go", "py", "js", "ts"]
# OR exclude specific extensions (ignored if include is set)
# exclude = ["min.js", "pb.go"]Extensions should not include a leading dot (use rs, not .rs).
CLI flags take precedence over config file settings:
| CLI Flag | Config Equivalent |
|---|---|
--comment-style <EXT:STYLE> |
[comment-styles] |
--include-ext <EXT> |
extensions.include |
--exclude-ext <EXT> |
extensions.exclude |
The --comment-style flag accepts the format EXT:STYLE where STYLE is a line-comment prefix:
edlicense --comment-style "java:// " --comment-style "xyz:# " .# .edlicense.toml
[comment-styles]
# Override Java to use line comments instead of block comments
java = { middle = "// " }
# Alias CommonJS to use the built-in JS style
cjs = "js"
[filenames]
"justfile" = { middle = "# " }
[extensions]
include = ["rs", "go", "py", "js", "ts", "java"]Check if all files have license headers without modifying them (dry run mode):
edlicense --dry-run src/ tests/Or simply (since dry run is the default):
edlicense src/ tests/Show diff of changes in dry run mode:
edlicense --show-diff src/ tests/Save diff of changes to a file in dry run mode:
edlicense --save-diff=changes.diff src/ tests/Show and save diff of changes:
edlicense --show-diff --save-diff=changes.diff src/ tests/Ignore specific file patterns:
edlicense --ignore "**/*.json" --ignore "vendor/**" .Use a specific year:
edlicense --year "2020" .Only check files that have changed relative to origin/main (dry run mode):
edlicense --ratchet "origin/main" src/Add or update license headers in files that have changed relative to origin/main:
edlicense --ratchet "origin/main" --modify src/Only process files tracked by git (this is the default when in a git repository):
edlicense --git-only src/Process all files, including those not tracked by git:
edlicense --git-only=false src/Unlike the original addlicense tool, edlicense can automatically update copyright year references when the year changes. For example, if a file contains:
Copyright (c) 2024 Example Corp
And the current year is 2025, running edlicense will update it to:
Copyright (c) 2025 Example Corp
To avoid accidentally replacing arbitrary 4-digit numbers, year updates only apply to recognized copyright patterns:
| Format | Year Updated? | Example |
|---|---|---|
Copyright (c) YEAR |
✅ Yes | Copyright (c) 2024 Acme |
Copyright © YEAR |
✅ Yes | Copyright © 2024 Acme |
Copyright YEAR |
❌ No | Copyright 2024 Acme |
| Year ranges | ❌ No | Copyright (c) 2020-2024 Acme |
The pattern matching is case-insensitive, so COPYRIGHT (C) 2024 works the same as Copyright (c) 2024.
Why these restrictions? The regex requires the (c) or © symbol to ensure we're matching an actual copyright statement, not some other 4-digit number in your code. Year ranges are intentionally preserved since updating 2020-2024 to 2025 would lose historical information.
If you use --preserve-years, no year updates will be performed regardless of format.
The ratchet mode allows you to only check and format files that have changed relative to a git reference (e.g., origin/main). This is particularly useful in CI/CD pipelines where you want to ensure that only new or modified files have proper license headers.
When using ratchet mode, edlicense will:
- Identify files that have been added, modified, or renamed since the specified git reference
- Only process those changed files, ignoring files that haven't changed
- Apply the same license checking or formatting rules to the changed files
This can significantly speed up processing in large repositories where only a small subset of files have changed.
Example usage:
# Only check license headers in files changed since origin/main (dry run mode)
edlicense --ratchet "origin/main" src/
# Add license headers to files changed since a specific commit
edlicense --ratchet "abc123" --modify --license-file LICENSE.txt src/You can use .licenseignore files to specify patterns for files that should be ignored during license checking and updates, similar to how .gitignore files work:
# Create a .licenseignore file in your project
echo "*.json" > .licenseignore
echo "vendor/" >> .licenseignore
echo "**/node_modules/" >> .licenseignore
# Run edlicense (it will automatically use the .licenseignore file)
edlicense src/You can also set a global ignore file using the GLOBAL_LICENSE_IGNORE environment variable:
export GLOBAL_LICENSE_IGNORE=/path/to/global/licenseignore
edlicense src/For more details and examples, see .licenseignore Files.
By default, when running in a git repository, edlicense will only process files that are tracked by git. This helps ensure that only files that are part of your project get license headers, while ignoring build artifacts, temporary files, and other untracked files.
Important: When git detection mode is enabled,
edlicenseuses your current working directory ($CWD) to determine whether it should only look at tracked files. You should always run edlicense from inside the git repository for correct operation.
You can control this behavior with the --git-only option:
# Only process files tracked by git (default when in a git repository)
edlicense --git-only src/
# Process all files, including those not tracked by git
edlicense --git-only=false src/This feature works well with the ratchet mode, allowing you to focus only on files that are both tracked by git and have changed since a specific reference:
# Only process files that are tracked by git and have changed since origin/main
edlicense --git-only --ratchet "origin/main" src/If you run edlicense from outside your git repository while using git detection mode, it will not be able to properly identify git-tracked files, which may result in no files being processed or incorrect files being processed.
For more details and examples, see Git Integration.
For information on using edlicense in pre-commit hooks, see Pre-commit Hooks.
edlicense supports a wide range of file types and automatically formats license headers with the appropriate comment style:
| Comment Style | File Extensions and Types |
|---|---|
Block comments/* ... */ |
.c, .h, .gv, .java, .scala, .kt, .kts |
JSDoc comments/** ... */ |
.js, .mjs, .cjs, .jsx, .tsx, .css, .scss, .sass, .ts |
Line comments// ... |
.cc, .cpp, .cs, .go, .hcl, .hh, .hpp, .m, .mm, .proto, .rs, .swift, .dart, .groovy, .v, .sv |
Hash comments# ... |
.py, .sh, .yaml, .yml, .rb, .tcl, .tf, .bzl, .pl, .pp, .toml |
Lisp comments;; ... |
.el, .lisp |
Erlang comments% ... |
.erl |
SQL/Haskell comments-- ... |
.hs, .sql, .sdl |
HTML comments<!-- ... --> |
.html, .xml, .vue, .wxi, .wxl, .wxs |
Jinja2 comments{# ... #} |
.j2 |
OCaml comments(** ... *) |
.ml, .mli, .mll, .mly |
In addition to extensions, edlicense also handles special file types by name:
cmakelists.txt,*.cmake.in,*.cmake: Hash comments (# ...)dockerfile,*.dockerfile: Hash comments (# ...)
edlicense includes performance tests to measure how efficiently it processes large numbers of files. These tests are useful for benchmarking and optimizing the tool's performance, especially for large codebases.
Performance tests are disabled by default since they generate and process thousands of files. To run them, use the following Makefile targets:
# Run test for adding licenses to 10,000 files
make perf-test-add
# Run test for updating license years in 10,000 files
make perf-test-update
# Run test for checking license headers in 10,000 files
make perf-test-check
# Run test with different file sizes
make perf-test-file-size
# Run test with different thread counts
make perf-test-threads
# Run comprehensive benchmark tests
make perf-benchmark
# Run all performance tests (this may take a while)
make perf-test-allAlternatively, you can run the tests directly with cargo:
# Run a specific performance test
cargo nextest run --release -E 'test(test_add_license_performance)' --run-ignored=all --no-capture
# Run all performance tests
cargo nextest run --release --run-ignored=all --no-captureThe performance tests measure:
- Adding licenses to large numbers of files (10,000+)
- Updating years in existing license headers
- Checking for licenses in check-only mode
- Impact of file size on processing time
- Impact of thread count on parallel processing performance
Results are displayed in the console with timing information, making it easy to identify performance bottlenecks or improvements.