Just-in-time credentials for your dev machine.
Documentation · Quickstart · Supported tools · Command reference · Security
Status: macOS-only (Apple Silicon), and still in development.
Your secrets live in plaintext all over your machine: .env files, ~/.aws/credentials, ~/.zshrc exports, .npmrc tokens, MCP configs. Anything running as you can read them. A bad curl | sh, a sketchy npm install, or one of the AI agents now running in your editor with your full permissions.
jit moves each secret into a local encrypted vault gated by Touch ID, and rewrites the files so your tools keep working. On disk there's now a decoy. The real value only appears, in memory, for the specific process that asked for it, after a biometric prompt. The result: you unlock once, jit asks before handing a credential to a tool (or an agent), and there's a decoy on disk the rest of the time.
| launched by Code | launched by claude |
|---|---|
![]() |
![]() |
brew install jitpass/tap/jitpass
Or without Homebrew:
curl -sL https://dl.jitpass.com/jitpass/jit/releases/latest/download/jitpass\_darwin\_arm64.tar.gz | tar -xz jit sudo mv jit /usr/local/bin/
Apple Silicon only — on an Intel Mac, build from source with go install github.com/jitpass/jit/cmd/jit@latest.
Pick one route. If you installed from the tarball before and are switching to Homebrew, remove the old copy after the brew install (sudo rm /usr/local/bin/jit); otherwise two jits sit on PATH upgrading separately, and jit doctor will flag it.
Releases are signed with a Developer ID and notarized by Apple, so both paths run without a Gatekeeper prompt: Homebrew quarantines its downloads and Gatekeeper clears them against the notarization ticket, while curl (and go install) set no quarantine flag at all. To check what you got rather than take our word for it, run jit doctor — its jit line reports signed CZC6BH93GJ, using the same check jit upgrade runs before it will install anything.
Upgrading: brew upgrade jitpass, or jit upgrade — a verified self-update (Developer-ID signature and checksum both checked before the swap, restarts the service). Either way your vault is untouched.
Homebrew installs shell completion with the binary, so jit <TAB> completes subcommands, flags, vault paths, and wrappable tool names out of the box. Installed from the tarball or from source, add it yourself:
echo 'source <(jit completion zsh)' >> ~/.zshrc && exec zsh
Either way, jit doctor tells you if completion isn't reaching your shell.
jit scan # read-only. changes no file it scans, prints no real value. jit vault init # make the vault (master key in your login keychain) jit migrate --dry-run # preview the whole machine-wide fix plan jit migrate # apply it: shows plan, asks [y/N], one Touch ID jit migrate ~/code/myapp # or fix just one project jit run -- npm run dev # run your tool; real values injected into that process only
jit scan with no path sweeps your whole home directory, so give it a moment on a large one. To go straight at one place, point it at a path: jit scan ~/.aws.
Day to day it's mostly jit run -- <cmd>. For CLIs that carry their own login token (gh, glab, stripe, and more) you jit wrap gh once and then keep typing gh as normal forever.
Not sure whether something needs jit wrap, jit migrate, or nothing? You don't have to know. jit scan splits everything it finds into what jit will protect (one command - the wraps included) and what only you can fix, and bare jit migrate runs that whole plan:
$ jit scan YOUR SECRETS: 7 — 0 protected by jit (0%) ▱▱▱▱▱▱▱▱▱▱ to 100%: one command +71% · 2 secrets only you can fix +29%
jit will protect these — 5 secrets in 4 files, 0% → 71% → jit migrate ~/.zshrc STRIPE_API_KEY, DB_PASSWORD ~/.config/gh/hosts.yml GitHub CLI token · wraps gh ...
only you can protect these — 2 secrets, 71% → 100%
\[rotate, then delete every copy\]
! A production database password in 2 files
→ rotate it now, then delete every copy
(jit scan --full still gives the classic per-category inventory with severities, including the Wrappable CLI Tokens section.)
Migrate the credential once, then keep using the tool the way you always have.
jit migrate ~/.aws/credentials # keys move to the vault; no plaintext file left aws s3 ls # resolves from the vault on demand. no prefix, no flag. terraform apply # same creds, same command
jit migrate ~/.config/gcloud/application_default_credentials.json terraform apply # google provider reads ADC; works after a Touch ID prompt
jit migrate ~/.docker/config.json # registry logins move to the vault jit run -- docker compose up # jit injects them for this run docker login ghcr.io # still works; the helper stores to the vault
jit migrate ~/.zshrc # leaves a one-line hook; new shells just have the vars ./deploy.sh # scripts that read those vars work unchanged
jit migrate ~/.zsh_history # each one moves to the vault; your commands stay, the secrets don't jit guard history # and stop the next one being recorded at all (zsh) # (bare `jit migrate` offers this too, in the plan it asks you to confirm)
jit wrap gh # one time gh pr list # token injected per call, forever
The first time each tool reaches for a real credential, jit asks once and remembers your answer until the vault locks. See Two Touch ID moments for how that sits on top of the vault unlock, what --trust does, and how to turn the per-tool prompts off.
Why do some tools need no setup while others take a jit run? One rule: can the tool ask jit for the secret itself? AWS (via credential_process), your shell at login, and docker's registry logins (via a credential helper) all can, so you type nothing extra. Tools that only read a file at runtime (docker compose, plain SDKs) can't ask, so jit run hands them the value.
The machine-global credential files (GCP ADC, sops, npm, netrc) work the same everyday way: run your tool and approve the per-process prompt. Add jit run --with <name> only when you want it explicit: for scripts and CI where there's no prompt to answer, or when you want a hard gate a project's own config can never reach. Supported tools lists exactly what to type for every tool, and how each is delivered.
jit asks for your fingerprint at two different moments, doing two different jobs:
jit after it locks, one Touch ID opens the vault for the whole session (5 minutes of activity, then it re-locks; and never longer than 8 hours, however busy you are). You unlock once, not once per command.jit asks before handing it over and names what's asking. This is what stops a program you didn't run from quietly using your keys while the vault is open.$ aws s3 ls Touch ID -> unlock your vault # gate 1: opens the vault for 5 min Touch ID -> aws wants your aws credential # gate 2: this tool, this credential ...your buckets...
$ aws s3 cp ./file s3://bucket/ # same tool, same session: no prompt
$ terraform apply Touch ID -> terraform wants your aws credential # a different tool: it asks on its own
Gate 2 is what keeps an unlocked vault from being a free-for-all: even after you've used aws yourself, a sketchy npm install reaching for those same keys still triggers a prompt naming it, so you can say no.
Don't want the second gate? Turn it off; the vault lock stays (turning it off itself takes a Touch ID, since it reopens the window it closes):
jit service consent off # tools resolve silently while the vault is unlocked jit service consent on # ask per tool again (the default)
Kicking off something that needs several credentials at once? jit run --trust -- terraform apply approves that whole run's tools in one gesture. Full details: per-process consent.
Both gates assume a human is there to answer. An AI agent working overnight, a long build, a scheduled job: the screen locks, the session drops, and the run stalls on a prompt nobody will see. A process grant moves your decision earlier instead of removing it - one Touch ID, given while you're still there, that names exactly what you're signing:
$ jit grant --process claude --profile jamf --for 8h Touch ID -> let claude under iTerm2 use 2 secrets (jamf) unattended for 8h ✓ granted g-7f3a2c81 claude -> jamf until 17:42 └ covers claude under iTerm2: 1 running now, any started before 17:42
For the next 8 hours, every claude under the terminal you typed that in (and what it launches) gets those secrets with no prompts - through screen lock and all, including sessions you start later: a new tab, the next claude, a script that fires at 3am. It's your terminal being named, not a name being trusted: a program calling itself claude somewhere else on the machine doesn't descend from that tree and inherits nothing. The grant ends at its deadline, when you quit that terminal, or the moment you type jit grant revoke (which needs no fingerprint - taking access away is always free). Want one exact process instead, gone when it exits? --pid. Every serve lands in the audit trail as its own event, so the morning after you can read exactly what your agent touched while you slept. Full details: process grants.
Every jit command and every unlock lands in a durable log you read back with jit audit, newest first, one key=value line per event, so it greps like a real service log. Command arguments are masked, so the log proves a command ran without ever storing the secret it carried.
$ jit audit --since 1h time=2026-07-24 10:15:04 level=info kind=cmd status=ok dur=312ms cmd="jit migrate ~/.aws/credentials" user=meni parent=claude time=2026-07-24 10:16:22 level=info kind=use op="read a secret" cmd="aws s3 ls" parent=claude secrets=aws/default time=2026-07-24 10:31:09 level=warn kind=unlock status=denied method=touchid-or-passcode cmd="node postinstall.js" parent=npm secrets=aws/default
The middle line is the story jit exists to tell: aws/default was read by aws s3 ls, launched by claude. The last is a prompt you declined: a node postinstall.js under npm reaching for those same keys, refused. jit also logs what the service turned away at its socket (a process the kernel says isn't yours, probing the agent) as kind=error.
Narrow it with flags instead of grep: --kind, --status ok|failed|denied, --since/--until (an age like 2h/3d or a date), --parent claude, --secret aws, --user, --grep <regexp>. Add --follow (-f) to stream new events live like tail -f, or --format json for a machine-parseable dump. Both halves are durable files beside the vault, so it answers for last week as readily as the last hour.
.env files, shell exports, AWS and Terraform, kubeconfig, Docker registry logins, GCP ADC, .npmrc / .netrc tokens, MCP server configs, bare token files, credentials recorded in your shell history, wrappable CLIs (gh, stripe, vercel, …), and SSO CLIs that mint credentials at login (clisso). In every case the file keeps working and the real value comes from the vault on demand.
The full catalog, grouped by exactly what to type for each tool, is Supported tools: it tracks the code as tools are added or removed. Anything not listed can still be wrapped with jit wrap add.
jit never destroys a credential. Migrate moves the value into the vault and leaves a working hook where it was (a decoy .env, an eval "$(jit export)" line in your shell config, credential_process = jit … in ~/.aws/config, or a PATH shim), so your tools keep resolving it on demand. The credential still exists, just encrypted instead of sitting in plaintext.
And every change is reversible. Before touching a file, jit backs it up encrypted into the vault, so jit migrate undo puts it back byte-for-byte:
jit migrate ~/code/myapp # applied the fix, one Touch ID
jit migrate undo ~/code/myapp # every touched file restored, byte-for-byte
The docs live under docs/, organized by task:
git commit -s), no CLAPolyForm Perimeter License 1.0.0 - free for personal and internal company use only.