npm.io
0.10.0 • Published yesterday

@git-sshripped/darwin-x64

Licence
MPL-2.0
Version
0.10.0
Deps
0
Size
4.8 MB
Vulns
0
Weekly
0
Stars
1

git-sshripped logo

git-sshripped

git-sshripped keeps selected files encrypted in Git while still letting you work with plaintext when the repository is unlocked.

It is for teams that already use SSH keys and want encryption to fit normal Git usage instead of adding a separate manual workflow.

Why this tool

  • SSH-native recipient access using existing SSH public/private keys.
  • Git-transparent encryption through Git filters, so normal git add, git commit, and git checkout behavior still applies.
  • Movable encrypted files by default, with opt-in path binding for patterns that should reject ciphertext moved from another path.
  • Worktree-aware lock/unlock state that behaves consistently across Git worktrees.
  • Built-in checks (doctor, verify --strict) to catch config issues and plaintext mistakes early.

Many alternatives fall short because they require manual encrypt/decrypt steps, do not use SSH as the recipient model, or do not behave cleanly with worktrees.

How it works

  1. init sets up manifest, patterns, and Git filter wiring.
  2. Protected paths (for example secrets/**) are encrypted when staged.
  3. The repository data key is wrapped to configured SSH recipients.
  4. unlock makes protected files readable in your working tree.
  5. lock removes unlocked key material and protected paths read back as ciphertext.

Quick start

Install via npm (no Rust required)
npm install git-sshripped

To auto-unlock encrypted files after npm install, add a postinstall script to your project:

{
  "scripts": {
    "postinstall": "git-sshripped unlock --soft"
  }
}

The --soft flag makes unlock non-fatal so npm install succeeds even when the user doesn't have access to encrypted files.

Install from source
cargo install git_sshripped_cli
Initialize a repository
# in an existing Git repository
git-sshripped init --strict --pattern "secrets/**" --recipient-key ~/.ssh/id_ed25519.pub

# use --path-binding strict for patterns that should reject encrypted blob moves
# git-sshripped init --pattern "prod-secrets/**" --path-binding strict --recipient-key ~/.ssh/id_ed25519.pub

# unlock using your SSH private key
git-sshripped unlock --identity ~/.ssh/id_ed25519

# work normally
mkdir -p secrets
printf 'API_TOKEN=example\n' > secrets/app.env
git add secrets/app.env

# validate setup
git-sshripped doctor
git-sshripped verify --strict

# lock when done
git-sshripped lock

Common commands

Daily use
  • git-sshripped unlock [--identity <path>] [--github-user <user>] [--prefer-agent] [--no-agent]

unlock auto-resolves an agent helper in this order:

  1. GSC_SSH_AGENT_HELPER env var
  2. git config --local git-sshripped.agentHelper
  3. .git-sshripped/config.toml (agent_helper)
  4. PATH search (git-sshripped-agent-helper, age-plugin-ssh-agent, age-plugin-ssh)

Helper contract: <helper> <wrapped-key-file> -> stdout with decrypted 32-byte repo key (raw bytes or 64-char hex).

  • git-sshripped lock
  • git-sshripped status
  • git-sshripped doctor [--json]
  • git-sshripped verify [--strict] [--json]
User and access management
  • git-sshripped add-user --key <pub|path>
  • git-sshripped add-user --github-user <user>
  • git-sshripped add-user --github-keys-url <url>
  • git-sshripped list-users [--verbose]
  • git-sshripped remove-user --fingerprint <fp> [--force]
  • git-sshripped revoke-user --fingerprint <fp> [--auto-reencrypt] [--json]
  • git-sshripped revoke-user --github-user <user> [--all-keys-for-user] [--auto-reencrypt] [--json]
  • git-sshripped revoke-user --org <org> --team <team> [--auto-reencrypt] [--json]
  • git-sshripped add-github-user --username <user> [--all] [--key <pub-key>] [--key-file <path>] [--no-auto-wrap]
  • git-sshripped list-github-users [--verbose]
  • git-sshripped remove-github-user --username <user> [--force]
  • git-sshripped refresh-github-keys [--username <user>] [--dry-run] [--fail-on-drift] [--json]
  • git-sshripped add-github-team --org <org> --team <team> [--no-auto-wrap]
  • git-sshripped list-github-teams
  • git-sshripped remove-github-team --org <org> --team <team>
  • git-sshripped refresh-github-teams [--org <org>] [--team <team>] [--dry-run] [--fail-on-drift] [--json]
  • git-sshripped access-audit [--identity <path>] [--json]

add-github-user and add-github-team auto-wrap by default when an unlock session is available. Use --no-auto-wrap to skip wrapping.

add-github-user filters fetched keys to only those matching a local private key in ~/.ssh/ by default. Pass --all to add every key associated with the GitHub account.

init can be run without recipients. The repo key is stored in a local session until a recipient is added (e.g. via add-github-user).

Movable files and path binding

New encrypted files are movable by default: ciphertext can be checked out at a new path and still decrypt. This keeps normal Git file moves from breaking checkout, rebase, or branch switching.

For patterns where encrypted blobs must not move between paths, opt into path binding:

git-sshripped init --pattern "prod-secrets/**" --path-binding strict

or set the attribute manually:

prod-secrets/** filter=git-sshripped diff=git-sshripped git-sshripped-path-binding=strict

Path-bound files use the legacy AES-SIV path-bound format. Movable files use the movable AES-SIV format. Existing repositories initialized before movable mode was the default can migrate by flipping the manifest default for future writes and re-encrypting the files already tracked at HEAD:

git-sshripped unlock
git-sshripped policy set --default-path-binding none
git-sshripped reencrypt   # one-time retrofit: re-clean tracked protected files with the new default
git commit -m "Switch to movable encryption"

The policy set step only changes the algorithm chosen for new clean-filter passes; reencrypt is what actually rewrites the currently-tracked ciphertext. History blobs stay in their original format, which is fine -- decrypt always dispatches on the per-blob algorithm ID.

Use --default-path-binding strict to make future writes path-bound unless a pattern-level attribute overrides it.

Maintenance
  • git-sshripped install
  • git-sshripped rewrap
  • git-sshripped rotate-key [--auto-reencrypt]
  • git-sshripped reencrypt
  • git-sshripped migrate-from-git-crypt [--dry-run] [--reencrypt] [--verify] [--json]
  • git-sshripped migrate-from-git-crypt ... [--write-report <path>]
  • git-sshripped export-repo-key --out <path>
  • git-sshripped import-repo-key --input <path>
  • git-sshripped policy show|set|verify [--json]
  • git-sshripped policy set --require-verify-strict-clean-for-rotate-revoke <true|false>
  • git-sshripped policy set --max-source-staleness-hours <hours>
  • git-sshripped policy set --default-path-binding <none|strict>
  • git-sshripped config set-agent-helper <path>
  • git-sshripped config set-github-api-base <url>
  • git-sshripped config set-github-web-base <url>
  • git-sshripped config set-github-auth-mode <auto|gh|token|anonymous>
  • git-sshripped config set-github-private-source-hard-fail <true|false>
  • git-sshripped config show

Debugging performance

git-sshripped ships optional span-level timing via the profiling crate with a tracing-backed subscriber. When disabled (the default) the instrumentation compiles to no-ops and adds zero runtime cost; enable it only when you need to investigate where time goes during a command.

Enable tracing

Build a release binary with the profile-trace feature:

cargo build --release --features git_sshripped_cli/profile-trace

Then run any subcommand with GIT_SSHRIPPED_TRACE=1 set. Span entries and their durations are written to stderr:

GIT_SSHRIPPED_TRACE=1 ./target/release/git-sshripped unlock --soft 2>trace.log

RUST_LOG is respected if you want to narrow which spans are emitted (for example RUST_LOG='info').

Run the benchmarks

The packages/cli/benches/unlock.rs harness uses criterion to measure the real-world cost of unlock --soft against a live repository. Real-repo benches are opt-in via environment variables so CI stays cheap:

export BENCH_REPO=$HOME/GitHub/monorepo             # an already-unlocked sshripped repo
export GIT_SSHRIPPED_BIN=$PWD/target/release/git-sshripped
cargo bench -p git_sshripped_cli --features profile-trace

Benches that require a missing variable print skipping and return, so it is safe to run the harness without setting them.

Security notes

  • This project is pre-1.0 and should be treated as security-sensitive software.
  • Deterministic encryption is used for Git filter stability and has known leakage tradeoffs.
  • Keep at least two valid recipients configured to reduce lockout risk.
  • git diff / git log -p for protected files. git-sshripped registers a textconv driver so git diff, git log -p, git show, and git blame show plaintext for unlocked repos. Git's textconv contract does not pass the original repo path to the textconv command (only a temp file), so path-bound (AesSivV1) ciphertext can't be decrypted from the temp file alone. The textconv reverse-resolves the path from the blob hash via git rev-list --objects --all and git ls-files -s, cached at <git-dir>/git-sshripped/textconv-paths.cache and invalidated when HEAD moves. The cache contains only public information (object IDs and repo paths) — never plaintext or key material. git-sshripped explicitly sets diff.git-sshripped.cachetextconv = false in install_git_filters because git's cachetextconv would persist the textconv plaintext output in .git/objects/info/cache, leaking decrypted secrets past git-sshripped lock. Locked repos pass the raw ciphertext through with a warning rather than aborting the surrounding Git command.

See SECURITY.md for the full threat model and operational guidance. See docs/COMPATIBILITY.md for git-crypt command mapping and migration notes.