HawkEye v7 is a rewrite. It does not accept v6 configuration aliases, and several behaviors changed in addition to the move from camel case to snake case. This guide targets v6.5.x and v7 and is written as an executable migration checklist for either a person or a code agent.
- Work on a clean branch and preserve the existing v6 config until the v7 result has been verified.
- Do not add compatibility aliases to the v7 config. Replace every v6 field and invocation at its source.
- Preserve the rendered license text, selected files, and failure policy unless this guide identifies an intentional v7 semantic change.
- Treat exit code 2 as a migration or runtime error. Exit code 1 from
checknormally means the config loaded successfully but one or more files need attention. - Review a dry-run report before allowing
formatto write files. Do not silently acceptconflictoutcomes.
Before editing, find every HawkEye input and integration:
find . -name 'licenserc.toml' -o -name '.licenserc.toml'
rg -n 'hawkeye|hawkeye-fmt|headerPath|inlineHeader|additionalHeaders|useDefault' .Record the following before conversion:
- The selected config file and its
baseDir. - Whether the header is inline, bundled, or loaded from a file.
- Every additional header-style file referenced by
additionalHeaders. - Custom entries under
[mapping.*]and whetheruseDefaultMappingis disabled. - Negated exclude patterns and directories that were covered only by v6 default excludes.
- CLI flags whose exit-code behavior is relied on by CI.
- GitHub Actions, Docker, pre-commit, Cargo, or library integrations.
The following v6 config:
baseDir = "."
headerPath = "Apache-2.0.txt"
strictCheck = true
useDefaultExcludes = true
useDefaultMapping = true
includes = ["**/*.rs", "**/*.toml"]
excludes = ["generated/**"]
keywords = ["copyright"]
[properties]
inceptionYear = 2022
copyrightOwner = "Acme Developers"
[git]
ignore = "auto"
attrs = "disable"becomes:
[header]
builtin = "Apache-2.0"
keywords = ["copyright"]
[files]
root = "{{ cwd }}"
includes = ["**/*.rs", "**/*.toml"]
excludes = ["generated/**"]
[props]
inception_year = 2022
copyright_owner = "Acme Developers"
[git]
ignore = "auto"
file_attrs = "disable"Use this field mapping for the rest of the config:
| v6 | v7 | Migration action |
|---|---|---|
baseDir |
files.root |
Move under [files]; use cwd for invocation-relative paths. |
inlineHeader |
header.text |
Move the template under [header]. |
headerPath |
header.builtin or header.path |
Use a built-in key for a bundled header; otherwise use a path. |
keywords |
header.keywords |
Move the list under [header]. |
includes |
files.includes |
Move the list under [files]. |
excludes |
files.excludes |
Move the list and rewrite any negation. |
properties |
props |
Move values and update every template reference. |
git.ignore |
git.ignore |
No field rename. |
git.attrs |
git.file_attrs |
Rename the field. |
[mapping.STYLE] |
[[rules]] |
Convert mappings to ordered rule entries. |
additionalHeaders |
[styles.NAME] |
Inline each referenced style in the main config. |
strictCheck |
none | Remove it; v7 uses safe literal style recognition. |
useDefaultExcludes |
none | Remove it and make required exclusions explicit. |
useDefaultMapping |
none | Remove it; built-in rules are always low-priority fallbacks. |
v6 evaluated a relative baseDir from the process working directory. v7 resolves a relative files.root from the config directory. To preserve invocation-relative scanning, use root = "{{ cwd }}" for v6's baseDir = ".". On Unix, use root = "{{ cwd }}/src" for baseDir = "src".
Relative header.path values also use only the config directory; v6 additionally tried baseDir and the process working directory. If a header relied on that fallback, choose its location explicitly, such as path = "{{ cwd }}/HEADER.txt" on Unix. See Path templates for shared configs and Windows-compatible path composition.
v7 requires exactly one header source:
header.builtin = "Apache-2.0"header.builtin = "Apache-2.0-ASF"header.builtin = "Elastic-2.0"header.path = "path/to/header.txt"header.text = "Copyright ..."
When v6 sets both inlineHeader and headerPath, it silently prefers the inline value. Preserve that result by keeping only header.text in v7.
The bundled v6 filenames lose their .txt suffix when used as v7 built-in keys. A custom file named Apache-2.0.txt must still use header.path if its content differs from the bundled template.
v6 converts every property to a string. v7 exposes the original TOML value under props, so integers remain integers, booleans remain booleans, and nested arrays or tables are allowed. Recheck comparisons, filters, and fallback expressions that assumed strings.
The v7 bundled Apache and Elastic templates use snake-case property names. Convert:
{{props["inceptionYear"]}} {{props["copyrightOwner"]}}to:
{{ props.inception_year }} {{ props.copyright_owner }}Custom property keys are not restricted, but migrating them to snake case makes the config consistent. Rename each key and every reference together.
v7 configures MiniJinja with strict undefined values. Missing properties now fail instead of rendering an empty value. File attributes that cannot be determined are null; express any fallback in the template rather than assuming the current year.
The available attributes are:
attrs.filenameattrs.disk_file_created_yearattrs.disk_file_modified_yearattrs.git_file_created_yearattrs.git_file_modified_yearattrs.git_authors
An empty files.includes list selects every discovered file. Both include and exclude patterns are relative to files.root and use Git-ignore-style matching.
v7 does not accept ! negation in either list. Rewrite a v6 exclusion such as build/** plus !build/keep.rs so the broad exclusion no longer covers the file that must remain selected. Do not copy the negated pattern verbatim.
v6 maintained a large default blacklist for build output, dependency directories, editor state, and metadata files. v7 removes that blacklist. Git ignore rules handle these paths in a normal repository; add project-specific files.excludes when generated or vendored files are not ignored, especially when git.ignore = "disable" or the scan runs outside a repository.
The header template selected by header.path is automatically excluded. .git is always excluded. v7 follows file symlinks but does not recurse through directory symlinks.
v6 mappings are keyed by style and stored without a useful declaration order. v7 rules are an ordered list. User rules run before built-in rules, and the first matching user rule wins.
Convert this v6 mapping:
[mapping.DOUBLESLASH_STYLE]
extensions = ["foo"]
filenames = ["Foo.config"]to:
[[rules]]
extensions = ["foo"]
filenames = ["Foo.config"]
style_out = "doubleslash"style_out is the canonical style written by format. An empty styles_in accepts only style_out. When existing files legitimately use more than one style, list the complete accepted set and include style_out:
styles_in = ["doubleslash", "slashstar"]Do not recreate a v6 mapping that is already covered by a v7 built-in rule unless the project needs to override its output or accepted styles. The current built-in selectors are documented in rules.toml.
v6 style names are case-insensitive; v7 names are case-sensitive. These common built-in names map as follows:
| v6 | v7 | v6 | v7 |
|---|---|---|---|
DOUBLESLASH_STYLE |
doubleslash |
SLASHSTAR_STYLE |
slashstar |
TRIPLESLASH_STYLE |
tripleslash |
JAVADOC_STYLE |
javadoc |
SCRIPT_STYLE |
script |
DOUBLEDASHES_STYLE |
doubledashes |
XML_STYLE |
xml |
XML_PER_LINE |
xml_per_line |
PERCENT_STYLE |
percent |
PERCENT3_STYLE |
percent3 |
SEMICOLON_STYLE |
semicolon |
APOSTROPHE_STYLE |
apostrophe |
EXCLAMATION_STYLE |
exclamation |
EXCLAMATION3_STYLE |
exclamation3 |
DOUBLETILDE_STYLE |
doubletilde |
BATCH |
batch |
HAML_STYLE |
haml |
BRACESSTAR_STYLE |
bracesstar |
SHARPSTAR_STYLE |
sharpstar |
MUSTACHE_STYLE |
mustache |
MVEL_STYLE |
mvel |
FTL |
ftl |
FTL_ALT |
ftl_alt |
DYNASCRIPT_STYLE |
dynascript |
DYNASCRIPT3_STYLE |
dynascript3 |
ASP |
asp |
PHP |
slashstar |
LUA |
lua |
ASCIIDOC_STYLE |
asciidoc |
LINE_BLOCK_STYLE |
line_block |
For SCALA_STYLE, JAVAPKG_STYLE, UNKNOWN, or any project-defined style, create a custom v7 style instead of guessing a built-in replacement. Even mapped built-ins may produce slightly different canonical whitespace, so inspect the dry-run diff.
Move every style from files named by additionalHeaders into the main config. A v6 line style:
[MY_STYLE]
multipleLines = false
beforeEachLine = "// "
afterEachLine = ""
padLines = falsebecomes:
[styles.my_style]
kind = "line"
prefix = "// "
suffix = ""
pad_lines = falseFor a v6 style with multipleLines = true, use kind = "block", map firstLine to start, beforeEachLine to prefix, afterEachLine to suffix, and endLine to end. Remove newline sentinels from start and end; v7 controls line endings itself.
v7 derives recognition from the literal line or block delimiters. It has no configurable equivalents for firstLineDetectionPattern, lastLineDetectionPattern, skipLinePattern, or allowBlankLines. It natively preserves a UTF-8 BOM, shebangs, XML and PHP declarations, HTML doctypes, YAML directives, and common Python or Ruby magic comments. If a project relies on another skipLinePattern, stop and report that case instead of silently dropping it.
strictCheck = false also has no direct replacement. v7 does not use whitespace-folded or similarity-based matching. A loosely matched v6 header may become replace when it is safely recognized or conflict when it is not safe to edit automatically.
Rename git.attrs to git.file_attrs. Both Git fields accept "disable", "auto", or "enable".
v7 defines Git creation as the latest addition in the current exact-path lifetime. Delete-and-recreate and rename-to-a-new-path both start a new lifetime. Merge history follows the parent state that produced the current file. This intentionally differs from treating every historical occurrence of the path as one continuous file.
git.file_attrs remains disabled by default because a long-lived file can require substantial history traversal. enable requires a complete, non-shallow repository; auto falls back when Git attributes are unavailable. v7 reads Git data in-process with gix and does not require a git executable at runtime.
| v6 | v7 |
|---|---|
--fail-if-unknown |
--fail-on-unknown |
--output report.json |
--output-format json > report.json |
check --fail-if-missing=true |
check already fails when a change is required |
check --fail-if-missing=false |
No equivalent; consume the JSON report if a non-failing audit is required |
format --fail-if-updated=true |
format --fail-on-change |
format --fail-if-updated=false |
format |
remove --fail-if-updated=true |
remove --fail-on-change |
remove --fail-if-updated=false |
remove |
The v7 format and remove commands succeed after making changes unless --fail-on-change is present. CI should normally use hawkeye check; use --fail-on-change only when an editing command must preserve the old failure policy.
v7 --dry-run performs no writes. v6 created sibling .formatted or .removed files; remove any workflow that reads those files and consume the human or JSON report instead.
All reports now go to stdout, while logs and errors go to stderr. JSON has one schema for every command:
{
"files": [
{
"path": "src/lib.rs",
"outcome": "replace"
}
]
}The possible outcomes are clean, add, replace, remove, conflict, and unsupported.
v7 also accepts files and directories after the subcommand. With no paths it scans the configured file set:
hawkeye check
hawkeye check src/lib.rs src/binThe v6 repository action is removed. Install and invoke the released CLI explicitly:
- uses: actions/checkout@v7
- uses: taiki-e/install-action@v2
with:
tool: hawkeye@7.0.0
- run: hawkeye checkUse the transferred image namespace and mount the repository at /workspace:
docker run --rm --user "$(id -u):$(id -g)" --volume "$PWD:/workspace" ghcr.io/fast/hawkeye:v7.0.0 checkUpdate the repository and revision:
repos:
- repo: https://github.com/fast/hawkeye
rev: v7.0.0
hooks:
- id: hawkeye-formatv7 hooks pass pre-commit's selected files to HawkEye. To retain a deliberate full-repository scan, set both pass_filenames: false and always_run: true in the consuming repository.
The separate hawkeye-fmt crate is replaced by the hawkeye library. A minimal dependency is:
hawkeye = { version = "7.0.0", default-features = false }The v6 callback and document APIs are not preserved. Build a Config, create an Engine, and select an explicit Scope:
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>(())Engine::format and Engine::remove return pending edits. Call apply to write them or into_report to inspect them without writing.
Install the exact release and confirm the selected binary:
cargo install hawkeye --version 7.0.0 --locked
hawkeye --versionThen verify in this order:
- Run
hawkeye check --output-format json > hawkeye-report.json. Exit code 1 is expected when files need changes; exit code 2 means the config or runtime still needs repair. - Inspect every
conflictandunsupportedentry. Fix the rule, style, template, or file selection rather than forcing an edit. - Run
hawkeye format --dry-run --output-format json > hawkeye-format.jsonand review alladdandreplaceoutcomes. - Ensure the Git worktree is clean, run
hawkeye format, and reviewgit difffor header text, whitespace, preambles, and line endings. - Run
hawkeye checkagain. It must exit 0. - Run the project's normal tests and its GitHub Actions, Docker, or pre-commit integration as applicable.
- Remove obsolete v6 style files and integration code only after the v7 check passes.
For a complete annotated v7 config, see the Configuration section in the README.