Skip to content

Repository files navigation

HawkEye

Crates.io Documentation MSRV 1.89 Apache 2.0 licensed Build Status

HawkEye checks, formats, and removes source-file license headers. The crate provides both the hawkeye command-line tool and a Rust library.

Installation

The recommended way to install the command-line tool is to let cargo-binstall download a prebuilt release:

cargo binstall hawkeye@7.0.0-alpha.1

To build from source instead, use Cargo:

cargo install hawkeye --version 7.0.0-alpha.1 --locked

Prebuilt releases cover the following platforms:

Distribution Platforms
cargo-binstall macOS and Linux on x86-64 or ARM64, plus Windows on x86-64; Linux archives support both glibc and musl.
Docker image Linux on amd64 or arm64.

Getting started

Add licenserc.toml to the project root. This minimal configuration uses the bundled Apache 2.0 header and the built-in file rules:

[header]
builtin = "Apache-2.0"

[props]
copyright_owner = "Acme Developers"
inception_year = 2026

Then check or update the project:

hawkeye check
hawkeye format

Command line

Command Behavior
hawkeye check Reports missing, non-canonical, and conflicting headers without writing files.
hawkeye format Adds missing headers and replaces recognized non-canonical headers.
hawkeye remove Removes recognized headers.

Pass files or directories after a command to avoid scanning the rest of a large repository:

hawkeye check src/lib.rs src/bin
git diff --name-only --diff-filter=ACMRT -z origin/main -- | xargs -0 -r hawkeye check --

Command-line paths are resolved from the current directory. They still obey files.root, files.includes, and files.excludes; an explicitly named file bypasses Git ignore rules, while a named directory uses normal discovery. Missing paths are skipped with a warning, while paths outside files.root are ignored as out of scope.

The git diff pipeline uses NUL-delimited paths so that unusual file names remain intact. Its -r option avoids running HawkEye with no arguments—and therefore avoids a full scan—when the diff is empty; the final -- keeps file names from being parsed as HawkEye options.

Without --config, HawkEye tries licenserc.toml and then .licenserc.toml in the current directory. It does not search parent directories.

All commands support --output-format json and --fail-on-unknown. format and remove also support --dry-run and --fail-on-change. Reports go to stdout; logs and errors go to stderr. Set RUST_LOG=hawkeye=debug to inspect file discovery and Git processing.

Exit code 0 means the selected policy passed. Exit code 1 means check found a required change or conflict, an edit command left a conflict, or an enabled failure option matched. Config, I/O, template, and Git errors use exit code 2.

Integrations

Docker

The distroless image runs HawkEye in /workspace. Pass the host user when formatting a bind mount so that writes retain the expected ownership:

docker run --rm --user "$(id -u):$(id -g)" --volume "$PWD:/workspace" ghcr.io/fast/hawkeye:v7.0.0-alpha.1 check

GitHub Actions

Install the released binary and invoke HawkEye directly; no HawkEye-specific action is required:

- uses: actions/checkout@v7
- uses: taiki-e/install-action@v2
  with:
    tool: hawkeye@7.0.0-alpha.1
- run: hawkeye check

pre-commit

The default hook installs the matching HawkEye source revision in pre-commit's isolated Python environment:

repos:
  - repo: https://github.com/fast/hawkeye
    rev: v7.0.0-alpha.1
    hooks:
      - id: hawkeye-format

The hooks pass only the files selected by pre-commit. To scan the complete configured file set whenever a hook runs, set pass_filenames: false on that hook in the project's .pre-commit-config.yaml; HawkEye then receives no paths and performs a normal full scan. Pre-commit skips a hook when no files match, so also set always_run: true when the full scan must run even in that case. The Python hook needs a Rust toolchain when its environment is created for the first time. Use hawkeye-format-docker instead when Docker is the preferred runtime.

Configuration

The following example shows every configuration section. Field names are snake case and unknown fields are rejected.

[header]
# Choose exactly one source. Built-in keys are case-sensitive.
builtin = "Apache-2.0"
# path = "HEADER.txt"
# text = "Copyright {{ props.inception_year }} {{ props.copyright_owner }}"

# Every keyword must occur, case-insensitively, before an existing comment can
# be replaced or removed. The default is ["copyright"].
keywords = ["copyright"]

[files]
# Relative paths are resolved from the directory containing this config file.
root = "."
# An empty includes list selects every discovered file.
includes = ["**/*.rs", "**/*.toml"]
excludes = ["generated/**"]

[props]
# Arbitrary TOML values are exposed to the header template as `props`.
copyright_owner = "Acme Developers"
inception_year = 2026

[git]
# Both fields accept "disable", "auto", or "enable".
ignore = "auto"
file_attrs = "disable"

[styles.quoted_line]
kind = "line"
prefix = "<!-- "
suffix = " -->"
pad_lines = true

[styles.quoted_block]
kind = "block"
start = "<!--"
prefix = "    "
suffix = ""
end = "-->"

[[rules]]
# User rules are matched in declaration order before built-in rules.
extensions = ["rs", "d.ts"]
filenames = ["Cargo.toml"]
# format writes one canonical style.
style_out = "doubleslash"
# An empty list accepts only style_out; otherwise list every accepted style.
styles_in = ["doubleslash", "slashstar"]

Headers and templates

header.builtin accepts Apache-2.0, Apache-2.0-ASF, or Elastic-2.0. header.path loads a UTF-8 template, while header.text stores the same template inline. A template file is never selected as a source file.

Templates use MiniJinja with strict undefined values, auto-escaping disabled, and its standard built-in filters, tests, and functions. HawkEye does not enable template includes or external loaders. The template context contains:

Value Meaning
props The complete user-defined [props] table.
attrs.filename The current file name.
attrs.disk_file_created_year The filesystem creation year, or null when unavailable.
attrs.disk_file_modified_year The filesystem modification year, or null when unavailable.
attrs.git_file_created_year The year the current exact-path lifetime began, or null when unavailable.
attrs.git_file_modified_year The latest year in the current exact-path history, including worktree changes.
attrs.git_authors Sorted distinct author names from the current exact-path history.

HawkEye never substitutes the current year for an unavailable value. Templates that need a fallback must express it explicitly.

Files, rules, and styles

files.includes and files.excludes use Git-ignore-style patterns relative to files.root; negated patterns are not accepted because inclusion and exclusion are separate lists. An empty includes list means all discovered files. .git is always excluded. File symlinks are followed, while directory symlinks are not traversed.

Rules match complete filenames or case-insensitive filename suffixes. Extensions omit the leading dot and may contain multiple segments, such as d.ts. The first matching user rule wins; built-in rules are lower-priority fallbacks. Duplicate selectors are allowed and logged at debug level when shadowed.

style_out is the canonical format written by HawkEye. styles_in lists formats that may be recognized and safely replaced or removed. When styles_in is empty, it defaults to [style_out]; a non-empty list must include style_out. Style names are case-sensitive. A custom style may override a built-in style and produces a warning. The bundled mappings are defined in rules.toml and styles.toml.

Git integration

git.ignore defaults to auto: it uses the Git index and ignore rules inside a worktree and falls back to filesystem discovery outside one. Tracked files remain selected even when they match an ignore rule. Set the mode to enable to require a worktree or disable to use filesystem discovery unconditionally.

git.file_attrs defaults to disable because resolving attributes may walk a long history for every selected path. Prefer fixed values in props unless the template genuinely needs repository history. auto populates attributes when complete history is available; enable requires a usable repository and complete history. Selecting paths on the command line also limits Git status and history work to those files.

Git attributes describe the current lifetime of an exact path. Deleting and later recreating a path starts a new history, as does moving a file to a new path; HawkEye does not infer identity from file similarity. At a merge, HawkEye follows a parent whose file entry matches the merge result. A merge resolution that differs from every parent counts as a modification and retains the contributing parent histories.

HawkEye reads repositories in-process with gix and does not require a git executable at runtime, including in the distroless container image. CI checkouts must still contain complete history when Git file attributes are enabled.

Library

The library exposes the same engine used by the command-line tool:

use hawkeye::Config;
use hawkeye::Engine;
use hawkeye::Scope;

let config = Config::load("licenserc.toml")?;
let engine = Engine::new(config)?;
let report = engine.check(Scope::All)?;
# Ok::<(), hawkeye::Error>(())

Each operation accepts a Scope. Scope::All processes the configured file set, while Scope::Paths(&paths) processes only the requested files and directories; an empty path slice processes nothing. Engine::check never writes files. Engine::format and Engine::remove return pending Edits; call Edits::apply to write them or Edits::into_report to inspect the result without writing.

The default application feature builds the command-line tool. Library-only users can omit its command-specific dependencies:

hawkeye = { version = "7.0.0-alpha.1", default-features = false }

Compatibility

HawkEye v7 uses a new snake-case configuration format and does not accept v6 field names. Follow MIGRATE.md to migrate a v6 configuration, command line, and integration.

Release summaries are recorded in CHANGELOG.md.

The minimum supported Rust version is 1.89.0. It may be raised in a minor release; patch releases preserve the minimum version of their corresponding minor release.

License

Licensed under the Apache License, Version 2.0.

About

Simple license header checker and formatter, in multiple distribution forms.

Topics

Resources

Stars

184 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages