git-xcrypt 0.2.0

Transparent, deterministic encryption of selected files in a git repository: plaintext in your working tree, ciphertext in the remote.
Documentation
[package]

name = "git-xcrypt"

version = "0.2.0"

edition = "2024"

# Measured, not guessed. Edition 2024 alone needs 1.85, but the code uses `let`

# chains in `if` conditions, which are stable only from 1.88 — on 1.85 the crate

# does not compile at all (12 × E0658). The whole suite passes on 1.88, so that

# is the floor, and the `msrv` job in CI holds it there: a bump has to be a

# decision, not a side effect of an edit nobody noticed.

rust-version = "1.88"

license = "MIT OR Apache-2.0"

description = "Transparent, deterministic encryption of selected files in a git repository: plaintext in your working tree, ciphertext in the remote."

repository = "https://github.com/rkarpin1/git-xcrypt"

homepage = "https://github.com/rkarpin1/git-xcrypt"

documentation = "https://docs.rs/git-xcrypt"

readme = "README.md"

keywords = ["git", "encryption", "secrets", "aes-siv", "cli"]

categories = ["command-line-utilities", "cryptography", "development-tools"]

# Working material for whoever builds this, not part of what is published:

# planning documents, IDE scopes, CI workflows, and the agent commands under

# `/.claude`. `CLAUDE.md` and `AGENTS.md` stay in the package deliberately —

# they are the rules of the crate, and they shipped with 0.1.0 and 0.1.1.

exclude = ["/context", "/.idea", "/.github", "/.claude"]



[lints.rust]

# AGENTS.md makes "zero unsafe" a hard rule; this is what enforces it.

unsafe_code = "forbid"



[dependencies]

# 0.8 is what pulls `aes` 0.9, which compiles the aarch64 backend

# unconditionally and picks it at *runtime* through `cpufeatures` — no opt-in

# cfg. That is what lets `.cargo/config.toml` go away, and with it the gap where

# `cargo install git-xcrypt` from a registry ran the software backend on Apple

# Silicon while a build from a clone did not.

#

# This rode `0.8.0-rc.3` from 2026-08-11 until 2026-08-12, when 0.8.0 landed and

# the requirement lifted itself onto it, exactly as recorded. There is now no

# pre-release package anywhere in the graph.

#

# Not one stored byte moves: all four frozen vectors in

# `tests/format_vectors.rs` reproduce byte for byte under 0.8, decrypt back, and

# RFC 5297 A.1 still matches. RFC 5297 is frozen, so the crate version cannot

# enter the format — but that is the argument, and the vectors are the proof.

aes-siv = "0.8.0"

base64 = "0.23"

bstr = "1.13.0"

clap = { version = "4.6.5", features = ["derive"] }

getrandom = "0.4.3"

# `status` has to answer "would git run our filter for this path" the way

# `git check-attr` answers it — nested `.gitattributes`, `info/attributes`,

# `[attr]` macros, last match wins. Naming suspicious lines instead was the last

# route to a green report on a repository that stores plaintext. Measured before

# adding it: `cargo add` locks exactly one package, because everything this

# crate needs is already in the graph through `gix-config` and `gix-glob`.

gix-attributes = "0.34.0"

gix-config = "0.59.0"

gix-discover = { version = "0.54.0", features = ["sha1"] }

gix-glob = "0.27.0"

gix-hash = { version = "0.26", features = ["sha1", "sha256"] }

# The history scan needs to read the object database itself: `status` has to

# answer whether a declared path ever sat in a commit in the clear, and no

# amount of working-tree inspection can. Both hash kinds, because `gitindex`

# already handles SHA-256 repositories and a scan that quietly skipped them

# would be the wrong kind of silence.

gix-object = { version = "0.63.0", features = ["sha1", "sha256"] }

gix-odb = { version = "0.83.0", features = ["sha1", "sha256"] }

gix-path = "0.12.4"

gix-ref = { version = "0.66.0", features = ["sha1", "sha256"] }

hkdf = "0.13.0"

sha2 = "0.11.0"

thiserror = "2.0.19"

zeroize = { version = "1.9.0", features = ["derive"] }



[dev-dependencies]

proptest = "1.11.0"

tempfile = "3.27.0"



# A shipped binary is what FR-011 promises, so the release profile is part of

# the product rather than a preference. `panic = "abort"` is deliberately *not*

# set: `lock` and `unlock` walk a working tree file by file, and an abort there

# would skip the reporting that tells a user which files were already rewritten.

[profile.release]

opt-level = 3

lto = "thin"

codegen-units = 1

strip = "symbols"