From d8b693bd3a97e23082c06766809e695fbe0875f1 Mon Sep 17 00:00:00 2001 From: Kun Lai Date: Mon, 31 Aug 2026 14:28:31 +0800 Subject: [PATCH 1/2] feat(cryptpilot-convert): pin systemd UEFI stub via --uki-stub-version Add a --uki-stub-version option (requires --uki) to control the source of the systemd UEFI stub (linuxx64.efi.stub) used to assemble the UKI: - "distro" (default) installs systemd-boot-unsigned from the distro repo, preserving the legacy version-floating behavior. - A version prefix (e.g. 261, 261.2-1) downloads a pinned, unsigned stub from the public Arch Linux Archive, resolved to the highest matching package. The prefix must be followed by a segment boundary ("." or "-") so a short prefix like "25" does not silently jump to 259.x; on no match the error lists the available versions. The pinned stub is downloaded (curl) and extracted (zstd + tar) on the host, placed at the canonical path dracut --uefi expects, then removed (restoring any pre-existing one) so the converted image's rootfs is byte-for-byte unchanged. The resolved exact version and stub sha256 are logged for measurement traceability. Different systemd-stub versions measure into PCR4/PCR8/PCR12 differently; pinning the stub keeps the UKI measurement reference values stable across builds. Also install curl/zstd/tar when the Arch path is selected, and preflight those tools with a clear actionable error. Docs (boot.md/boot_zh.md, quick-start) updated. Verified end-to-end on an Alinux 4 image: --uki-stub-version 261 resolves to systemd-261.2-1 (sha256 2d9b8073...) and 260 to systemd-260.2-2 (sha256 26e9f431...), yielding distinct UKI measurement.uki.SHA-384 values; the embedded stub in the produced qcow2 matches the expected Arch stub byte-for-byte (.sbat). Signed-off-by: Kun Lai Assisted-by: Claude:glm-5.2 --- cryptpilot-convert.sh | 223 ++++++++++++++++++++++++-- cryptpilot-fde/docs/boot.md | 5 + cryptpilot-fde/docs/boot_zh.md | 5 + cryptpilot-fde/docs/quick-start.md | 1 + cryptpilot-fde/docs/quick-start_zh.md | 1 + 5 files changed, 218 insertions(+), 17 deletions(-) diff --git a/cryptpilot-convert.sh b/cryptpilot-convert.sh index 76b96b9..786595f 100755 --- a/cryptpilot-convert.sh +++ b/cryptpilot-convert.sh @@ -225,6 +225,13 @@ proc::print_help_and_exit() { echo " --uki-append-cmdline Append custom command line parameters when generating a UKI image. By default, only essential" echo " parameters are included. This option allows you to extend the kernel command line. The default" echo " value is 'console=tty0 console=ttyS0,115200n8'." + echo " --uki-stub-version Source of the systemd UEFI stub (linuxx64.efi.stub) used to assemble the UKI." + echo " Only meaningful with --uki. 'distro' (default) installs systemd-boot-unsigned" + echo " from the distro repo (version floats with the distro). Any other value is a" + echo " version prefix resolved against the public Arch Linux Archive to the highest" + echo " matching package, e.g. '261' -> latest 261.x, '261.2-1' -> that exact package" + echo " (fully pinned, so the PCR measurement reference stays stable). A pinned stub" + echo " is downloaded, used, and removed so the converted image is unchanged." echo " -h, --help Show this help message and exit." exit "$1" } @@ -1044,24 +1051,157 @@ EOF } +# --- efi stub provisioning (systemd-stub / linuxx64.efi.stub) ------------------- +# +# dracut --uefi assembles a UKI by linking kernel+initrd+cmdline with the +# systemd UEFI stub at /usr/lib/systemd/boot/efi/linuxx64.efi.stub. Different +# systemd-stub versions measure into PCR4/PCR8/PCR12 differently, so pinning +# the stub makes the measurement reference values stable. +# +# --uki-stub-version controls the source: +# distro install systemd-boot-unsigned from the distro's own +# repo (version floats with the distro; the legacy +# behavior). This is the default when --uki is given +# without --uki-stub-version. +# download a pinned stub from the public Arch Linux +# Archive. The prefix is resolved against the archive +# directory listing to the highest matching package: +# "261" -> latest 261.x (e.g. 261.2-1) +# "261.2" -> 261.2-1 +# "261.2-1" -> 261.2-1 (exact, fully pinned) +# A prefix must be followed by a segment boundary +# ("." or "-"), so "25" does NOT silently resolve to +# 259.x -- it errors and lists what is available. +readonly STUB_SPECIAL_DISTRO="distro" +readonly STUB_REL_PATH="usr/lib/systemd/boot/efi/linuxx64.efi.stub" +readonly ARCH_STUB_ARCHIVE_URL="https://archive.archlinux.org/packages/s/systemd/" +readonly ARCH_STUB_MEMBER="usr/lib/systemd/boot/efi/linuxx64.efi.stub" + +# Print the base systemd package filenames available in the Arch Linux Archive, +# newest-last. Excludes subpackages (systemd-libs, -sysvcompat, ...) because +# those start with a letter after "systemd-", and excludes .sig files. +_arch_stub_list_pkgs() { + curl -sS --max-time 60 "${ARCH_STUB_ARCHIVE_URL}" \ + | grep -oE "systemd-[0-9][^\"<>[:space:]]*-x86_64\.pkg\.tar\.zst" \ + | grep -v '\.sig$' \ + | sort -uV +} + +# Resolve a version prefix to the exact Arch package filename matching it, or +# fail with a suggestion list of available versions. The match requires the +# prefix to be followed by a segment boundary so short prefixes like "25" do +# not jump across major versions (e.g. to 259.x). +resolve_arch_stub_pkg() { + local prefix="$1" + local pkgs best="" + local rest nxt + + pkgs="$(_arch_stub_list_pkgs)" + if [ -z "$pkgs" ]; then + echo "ERROR: could not fetch the systemd package list from the Arch Linux Archive (${ARCH_STUB_ARCHIVE_URL})." >&2 + echo " Check network connectivity, then retry." >&2 + return 1 + fi + + while IFS= read -r p; do + [ -z "$p" ] && continue + rest="${p#systemd-}" # e.g. 261.2-1-x86_64.pkg.tar.zst + # literal (non-regex) prefix match on the version portion + if [ "${rest:0:${#prefix}}" = "$prefix" ]; then + nxt="${rest:${#prefix}:1}" + if [ "$nxt" = "." ] || [ "$nxt" = "-" ] || [ -z "$nxt" ]; then + best="$p" # keep last; pkgs are already newest-last + fi + fi + done <<<"$pkgs" + + if [ -n "$best" ]; then + echo "$best" + return 0 + fi + + # No match: suggest the most recent available major versions. + local available + available=$(printf '%s\n' "${pkgs}" \ + | sed -E 's/^systemd-([0-9]+)(\.|-).*/\1/' \ + | sort -un | tail -n 8 | awk '{printf "%s.x ", $1}' | sed 's/ $//') + echo "ERROR: efi stub version '${prefix}' did not match any package in the Arch Linux Archive." >&2 + echo " Available systemd versions: ${available:-none}" >&2 + echo " Pass a major version (e.g. --uki-stub-version 261) or an exact pkgver-pkgrel (e.g. 261.2-1)." >&2 + return 1 +} + step:update_initrd() { local efi_part=$1 local boot_file_path=$2 local uki=$3 local uki_append_cmdline=$4 + local uki_stub_version=$5 update_initrd_inner() { local rootfs_mount_point=$1 local uki=$2 local uki_append_cmdline=$3 + local uki_stub_version=$4 # Copy files to the chroot environment cp "${workdir}/metadata.toml" "${rootfs_mount_point}/tmp/" mkdir -p "${rootfs_mount_point}/tmp/cryptpilot/" cp -a "${config_dir}/." "${rootfs_mount_point}/tmp/cryptpilot/" + + # When a pinned stub version is requested (anything other than the + # "distro" sentinel), download it from the public Arch Linux Archive + # and place it at the canonical path dracut --uefi expects. The stub + # is removed (and any pre-existing one restored) after the UKI is + # built below, so the converted image's rootfs is left byte-for-byte + # unchanged. The distro path instead installs the package inside the + # chroot and leaves it in place. + local stub_placed=false + local stub_backup="" + local stub_path="${rootfs_mount_point}/${STUB_REL_PATH}" + if [ "${uki:-false}" = true ] && [ "${uki_stub_version}" != "${STUB_SPECIAL_DISTRO}" ]; then + # Verify the host tools needed to download/extract/log a pinned stub + # (a .pkg.tar.zst). Step 0 tries to install them, but check here so a + # missing tool is reported clearly instead of a confusing extract error. + local _missing _t + _missing="" + for _t in curl zstd tar sha256sum; do + command -v "$_t" >/dev/null 2>&1 || _missing="${_missing} ${_t}" + done + if [ -n "$_missing" ]; then + proc::fatal "host tools required for --uki-stub-version are missing:${_missing} (install zstd, tar, curl, coreutils)" + fi + local pkg tmp_pkg + if ! pkg=$(resolve_arch_stub_pkg "${uki_stub_version}"); then + proc::fatal "could not resolve efi stub version '${uki_stub_version}'" + fi + mkdir -p "$(dirname "${stub_path}")" + if [ -e "${stub_path}" ]; then + stub_backup="${stub_path}.cryptpilot.orig" + mv "${stub_path}" "${stub_backup}" + fi + log::info "Downloading pinned efi stub from Arch Linux Archive: ${pkg}" + tmp_pkg=$(mktemp) + if ! curl -sS --max-time 120 -o "${tmp_pkg}" "${ARCH_STUB_ARCHIVE_URL}${pkg}"; then + rm -f "${tmp_pkg}" + [ -n "${stub_backup}" ] && mv "${stub_backup}" "${stub_path}" + proc::fatal "failed to download efi stub ${pkg} from the Arch Linux Archive" + fi + if ! zstd -d -c "${tmp_pkg}" 2>/dev/null | tar -xOf - "${ARCH_STUB_MEMBER}" > "${stub_path}" 2>/dev/null || [ ! -s "${stub_path}" ]; then + rm -f "${tmp_pkg}" "${stub_path}" + [ -n "${stub_backup}" ] && mv "${stub_backup}" "${stub_path}" + proc::fatal "failed to extract efi stub ${ARCH_STUB_MEMBER} from ${pkg}" + fi + rm -f "${tmp_pkg}" + local stub_sha + stub_sha=$(sha256sum "${stub_path}" | cut -d' ' -f1) + log::info "resolved efi stub: ${pkg} (from prefix '${uki_stub_version}'), sha256=${stub_sha}" + stub_placed=true + fi + # update initrd log::info "Updating initrd" - chroot "${rootfs_mount_point}" bash -c "uki='${uki}' ; uki_append_cmdline='${uki_append_cmdline}' ; $( + chroot "${rootfs_mount_point}" bash -c "uki='${uki}' ; uki_append_cmdline='${uki_append_cmdline}' ; uki_stub_version='${uki_stub_version}' ; $( cat <<'EOF' set -e set -u @@ -1099,18 +1239,31 @@ if [[ -f /tmp/cryptpilot/global.toml ]]; then fi if [ "${uki:-false}" = true ]; then - # dracut --uefi needs the systemd UEFI stub (linuxx64.efi.stub) to assemble - # a UKI. On Alinux 3 this ships in the enabled systemd-udev package; on - # Alinux 4 it lives in systemd-boot-unsigned, which is only in the disabled - # *-devel repo. Install it on demand so UKI works across distros; this is - # a no-op when the stub is already present. - if [ ! -e /usr/lib/systemd/boot/efi/linuxx64.efi.stub ]; then - echo "EFI stub linuxx64.efi.stub not found; installing systemd-boot-unsigned" - # mirrors.cloud.aliyuncs.com is only reachable from inside Alibaba - # Cloud; switch to the public mirror so the install works in CI and - # other non-Aliyun environments too. - sed -i 's|mirrors.cloud.aliyuncs.com|mirrors.aliyun.com|g' /etc/yum.repos.d/*.repo 2>/dev/null || true - yum --enablerepo='*devel*' install -y systemd-boot-unsigned || yum install -y systemd-boot-unsigned + # dracut --uefi needs the systemd UEFI stub (linuxx64.efi.stub) at + # /usr/lib/systemd/boot/efi/ to assemble a UKI. Two sourcing modes, + # selected by uki_stub_version (the literal "distro" sentinel must match + # the host-side STUB_SPECIAL_DISTRO): + # "distro" -> install systemd-boot-unsigned from the distro repo on + # demand (legacy behavior; version floats). On Alinux 3 + # the stub ships with systemd-udev; on Alinux 4 it is + # in the disabled *-devel repo. + # -> a pinned stub was already downloaded from the Arch + # Linux Archive and placed at the canonical path by the + # host before entering the chroot; just verify it. + if [ "${uki_stub_version:-distro}" = "distro" ]; then + if [ ! -e /usr/lib/systemd/boot/efi/linuxx64.efi.stub ]; then + echo "EFI stub linuxx64.efi.stub not found; installing systemd-boot-unsigned" + # mirrors.cloud.aliyuncs.com is only reachable from inside Alibaba + # Cloud; switch to the public mirror so the install works in CI and + # other non-Aliyun environments too. + sed -i 's|mirrors.cloud.aliyuncs.com|mirrors.aliyun.com|g' /etc/yum.repos.d/*.repo 2>/dev/null || true + yum --enablerepo='*devel*' install -y systemd-boot-unsigned || yum install -y systemd-boot-unsigned + fi + else + if [ ! -e /usr/lib/systemd/boot/efi/linuxx64.efi.stub ]; then + echo "ERROR: pinned efi stub (uki_stub_version='${uki_stub_version}') was not placed at /usr/lib/systemd/boot/efi/linuxx64.efi.stub" >&2 + exit 1 + fi fi # Remove all existing EFI entries @@ -1150,13 +1303,23 @@ fi EOF )" + # Remove the pinned stub (and restore any pre-existing one) so the + # converted image's rootfs is byte-for-byte unchanged. Only the Arch + # path places a stub here; the distro path leaves its package install + # in place. + if [ "${stub_placed}" = true ]; then + rm -f "${stub_path}" + if [ -n "${stub_backup}" ] && [ -e "${stub_backup}" ]; then + mv "${stub_backup}" "${stub_path}" + fi + fi } # Remove read-only flag from rootfs.img tune2fs -O ^read-only "${rootfs_file_path}" # Note that the rootfs.img will not be used any more so mount it without '-o ro' flag will not change the hash of rootfs. - run_in_chroot_mounts "$rootfs_file_path" "$efi_part" "$boot_file_path" update_initrd_inner "$uki" "$uki_append_cmdline" + run_in_chroot_mounts "$rootfs_file_path" "$efi_part" "$boot_file_path" update_initrd_inner "$uki" "$uki_append_cmdline" "$uki_stub_version" } step::shrink_and_extract_rootfs_part() { @@ -1342,6 +1505,7 @@ main() { local wipe_freed_space=false local uki=false local uki_append_cmdline="console=tty0 console=ttyS0,115200n8" + local uki_stub_version="distro" while [[ "$#" -gt 0 ]]; do case $1 in @@ -1393,6 +1557,10 @@ main() { uki_append_cmdline="$2" shift 2 ;; + --uki-stub-version) + uki_stub_version="$2" + shift 2 + ;; -h | --help) proc::print_help_and_exit 0 ;; @@ -1402,6 +1570,19 @@ main() { esac done + # Validate --uki-stub-version: either the "distro" sentinel or a numeric + # Arch version prefix (digits, dots, hyphens). The char-class check also + # prevents single-quote injection since the value is interpolated into the + # chroot bash -c command string. + if [ "${uki}" = true ] && [ "${uki_stub_version}" != "distro" ]; then + if ! [[ "${uki_stub_version}" =~ ^[0-9][0-9.-]*$ ]]; then + proc::fatal "Invalid --uki-stub-version '${uki_stub_version}': use 'distro' or a version prefix like '261' / '261.2-1'" + fi + elif [ "${uki}" = false ] && [ "${uki_stub_version}" != "distro" ]; then + log::warn "--uki-stub-version is ignored without --uki" + uki_stub_version="distro" + fi + if [ -n "${device:-}" ]; then if [ -n "${input_file:-}" ] || [ -n "${output_file:-}" ]; then proc::fatal "Cannot specify both --device and --in/--out" @@ -1478,6 +1659,11 @@ main() { if [[ "$uki" == "true" ]]; then tool_packages+=(grub2-tools) # Required for UKI (Unified Kernel Image) boot setup fi + # A pinned stub from the Arch Linux Archive is a .pkg.tar.zst, so the + # Arch path needs curl (download), zstd + tar (extract), sha256sum (log). + if [[ "$uki" == "true" && "$uki_stub_version" != "distro" ]]; then + tool_packages+=(curl zstd tar) + fi apt-get update apt-get install -y "${tool_packages[@]}" else @@ -1485,6 +1671,9 @@ main() { if [[ "$uki" == "true" ]]; then tool_packages+=(grub2-tools) # Required for UKI (Unified Kernel Image) boot setup fi + if [[ "$uki" == "true" && "$uki_stub_version" != "distro" ]]; then + tool_packages+=(curl zstd tar) + fi yum install -y "${tool_packages[@]}" fi @@ -1633,12 +1822,12 @@ main() { # log::step "[ 9 ] Update initrd" if [ "$boot_part_exist" = "true" ]; then - step:update_initrd "${efi_part}" "${boot_part}" "${uki}" "${uki_append_cmdline}" + step:update_initrd "${efi_part}" "${boot_part}" "${uki}" "${uki_append_cmdline}" "${uki_stub_version}" else if [ "$uki" = true ]; then - step:update_initrd "${efi_part}" "" "${uki}" "${uki_append_cmdline}" + step:update_initrd "${efi_part}" "" "${uki}" "${uki_append_cmdline}" "${uki_stub_version}" else - step:update_initrd "${efi_part}" "${boot_part}" "${uki}" "${uki_append_cmdline}" + step:update_initrd "${efi_part}" "${boot_part}" "${uki}" "${uki_append_cmdline}" "${uki_stub_version}" fi fi diff --git a/cryptpilot-fde/docs/boot.md b/cryptpilot-fde/docs/boot.md index 86bd504..c818a61 100644 --- a/cryptpilot-fde/docs/boot.md +++ b/cryptpilot-fde/docs/boot.md @@ -126,6 +126,11 @@ flowchart LR UKI generation uses dracut's `--uefi` parameter. The default kernel command line is `console=tty0 console=ttyS0,115200n8`, and custom parameters can be appended via `--uki-append-cmdline`. `cryptpilot-fde-host` parses segments in the UKI image directly when calculating reference values. +**EFI Stub Source (`--uki-stub-version`)**: dracut assembles the UKI by linking the kernel, initrd, and cmdline with the systemd UEFI stub (`linuxx64.efi.stub`). Different systemd-stub versions measure into PCR4/PCR8/PCR12 differently, so pinning the stub keeps the measurement reference values stable across builds. + +- `--uki-stub-version distro` (default): installs `systemd-boot-unsigned` from the distro's own repo. The version floats with the distro, so PCR reference values are only stable as long as the distro package does not change. +- `--uki-stub-version `: downloads a pinned, unsigned stub from the public [Arch Linux Archive](https://archive.archlinux.org/packages/s/systemd/). The value is a version prefix resolved to the highest matching package — `261` selects the latest `261.x`, `261.2-1` selects that exact package (fully pinned). The stub is downloaded, used to build the UKI, then removed, so the converted image is left unchanged. The resolved exact version and stub SHA-256 are logged, which lets you fix an exact value after verifying once. + ### 3.3 Mode Comparison | Feature | GRUB Mode | UKI Mode | diff --git a/cryptpilot-fde/docs/boot_zh.md b/cryptpilot-fde/docs/boot_zh.md index e8295a7..e77e287 100644 --- a/cryptpilot-fde/docs/boot_zh.md +++ b/cryptpilot-fde/docs/boot_zh.md @@ -126,6 +126,11 @@ flowchart LR UKI生成使用dracut的`--uefi`参数,默认内核命令行为`console=tty0 console=ttyS0,115200n8`,可通过`--uki-append-cmdline`追加自定义参数。`cryptpilot-fde-host`在计算参考值时直接解析UKI镜像中的各段进行度量。 +**EFI Stub 来源(`--uki-stub-version`)**:dracut 通过将内核、initrd、命令行与 systemd 的 UEFI stub(`linuxx64.efi.stub`)链接来组装 UKI。不同版本的 systemd-stub 向 PCR4/PCR8/PCR12 的度量行为不同,因此钉死 stub 版本可使度量参考值在多次构建间保持稳定。 + +- `--uki-stub-version distro`(默认):从发行版自身仓库安装 `systemd-boot-unsigned`。版本随发行版浮动,因此 PCR 参考值仅在发行版包不变时才稳定。 +- `--uki-stub-version `:从公开的 [Arch Linux Archive](https://archive.archlinux.org/packages/s/systemd/) 下载一个钉死的 unsigned stub。取值为版本前缀,解析为最高匹配的包——`261` 选最新 `261.x`,`261.2-1` 选该精确包(完全钉死)。stub 下载后用于构建 UKI,随后删除,故转换后的镜像保持不变。解析到的精确版本与 stub 的 SHA-256 会被记录到日志,便于你验证一次后把精确值固化下来。 + ### 3.3 模式对比 | 特性 | GRUB模式 | UKI模式 | diff --git a/cryptpilot-fde/docs/quick-start.md b/cryptpilot-fde/docs/quick-start.md index bf15257..cb6ad78 100644 --- a/cryptpilot-fde/docs/quick-start.md +++ b/cryptpilot-fde/docs/quick-start.md @@ -270,6 +270,7 @@ cryptpilot-convert --in ./aliyun_3_x64_20G_nocloud_alibase_20251030.qcow2 \ - `--rootfs-no-encryption`: rootfs measure-only without encryption - `--uki`: Generate UKI unified kernel image +- `--uki-stub-version `: Source of the systemd UEFI stub used to assemble the UKI. `distro` (default) installs it from the distro repo; a version prefix like `261` or `261.2-1` pins a stub from the Arch Linux Archive so PCR reference values stay stable. ### Calculate Reference Values diff --git a/cryptpilot-fde/docs/quick-start_zh.md b/cryptpilot-fde/docs/quick-start_zh.md index fe28390..d38f26d 100644 --- a/cryptpilot-fde/docs/quick-start_zh.md +++ b/cryptpilot-fde/docs/quick-start_zh.md @@ -270,6 +270,7 @@ cryptpilot-convert --in ./aliyun_3_x64_20G_nocloud_alibase_20251030.qcow2 \ - `--rootfs-no-encryption`:rootfs 仅度量不加密 - `--uki`:生成 UKI 统一内核镜像 +- `--uki-stub-version `:组装 UKI 所用的 systemd UEFI stub 来源。`distro`(默认)从发行版仓库安装;传版本前缀如 `261` 或 `261.2-1` 则从 Arch Linux Archive 钉死一个 stub,使 PCR 参考值保持稳定。 ### 计算参考值 From b899655381c0d5931a041f24b37396e946489520 Mon Sep 17 00:00:00 2001 From: Kun Lai Date: Mon, 31 Aug 2026 14:51:16 +0800 Subject: [PATCH 2/2] docs(claude.md): formalize git commit requirements Document the project's commit conventions in the Git Commit Requirements section: - Require a Signed-off-by trailer with the author's own identity on every commit (git commit -s, or added by hand). - Require an Assisted-by trailer (AGENT_NAME:MODEL_VERSION [TOOLS]) as the only accepted form of AI attribution on Claude-authored commits. - Never commit plan/spec files or anything that is gitignored. - Never hand-edit cryptpilot.spec Version/Release/%changelog; version and changelog are produced at release time by make bump-version-{major,minor,patch}. Signed-off-by: Kun Lai Assisted-by: Claude:glm-5.2 --- CLAUDE.md | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index eccf43a..d8803cd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,9 +18,26 @@ When creating or amending commits: - **Author and committer** must always be taken from the local git config (`git config user.name` / `git config user.email`). Never use Claude's own identity. - **Never** add `Co-Authored-By:` trailers of any kind. +- **Always** add a `Signed-off-by:` trailer with the author's own identity, taken from the local git config (`Signed-off-by: Your Name `). Use `git commit -s` (which appends it from the configured identity) or add it by hand at the end of the commit message. This Developer Certificate of Origin trailer is required on every commit. +- **Always** add an `Assisted-by:` trailer to every commit message that Claude authored or co-authored. The trailer is the *only* accepted form of AI attribution. Format: + + ``` + Assisted-by: AGENT_NAME:MODEL_VERSION [TOOL1] [TOOL2] ... + ``` + + Where `AGENT_NAME` is the AI tool name (e.g. `Claude`), `MODEL_VERSION` is the specific model version used (e.g. `claude-opus-4-8`), and the optional bracketed `[TOOL]` entries are specialized analysis tools employed in producing the change (e.g. `coccinelle`, `sparse`, `smatch`, `clang-tidy`). Basic development tools (`git`, `gcc`, `make`, editors) must **not** be listed. Place the trailer as the last line(s) of the commit message body, separated by a blank line from the rest of the message. Example: + + ``` + Assisted-by: Claude:claude-opus-4-8 clang-tidy + ``` + + Only one `Assisted-by:` trailer per commit. If no specialized tool was used, omit the bracketed list entirely (`Assisted-by: Claude:claude-opus-4-8`). - **Never** include any Claude session URLs, session IDs, or links to claude.ai in commit messages or PR descriptions. Commit messages should only describe the code changes. -- **Never** include `🤖 Generated with [Claude Code](https://claude.com/claude-code)` or similar AI assistant references in PR descriptions or commit messages. +- **Never** include "🤖 Generated with [Claude Code](https://claude.com/claude-code)" or similar AI attribution footers in PR descriptions or commit messages. - **Always** use `--no-gpg-sign` to avoid GPG signing. +- **Never commit plan or spec files** (e.g. `docs/*-plan.md`, `docs/*-design.md`, `docs/*-spec.md`, or anything under `docs/superpowers/`). These should be gitignored (already covered by `.gitignore`) and kept local only. +- **Never commit any file that is already gitignored** — if a file matches `.gitignore`, it is intentionally local-only. +- **Never manually edit version information in `cryptpilot.spec`** — do not touch the `Version:` or `Release:` fields, and do not add version-stamped `%changelog` entries by hand. Version/release bumps across the whole repo (`Cargo.toml`, `Cargo.lock`, `APPLICATION/*/buildspec.yml`, debian build files, and the RPM spec `Version` + `%changelog`) are produced at a specific release stage by `make bump-version-{major,minor,patch}`. That target regenerates the spec changelog from commit subjects since the last tag, so a hand-written entry would both carry a wrong release number and duplicate the auto-collected commits. Edit the spec only for packaging logic (e.g. `BuildRequires`/`Requires`, `%build` flags); leave versioning to `make bump-version-*`. ## Pre-Commit Checks