kc-cli 0.6.0

CLI for reading and writing macOS keychain files without the Security framework
# kc-cli

`kc` creates, queries, and updates macOS `.keychain-db` files directly. It does
not call the Security framework, require entitlements, or depend on a running
`securityd`. The files remain interoperable with Keychain Access and
`/usr/bin/security`.

The Rust library is [`keychain-rs`](../keychain/README.md). This crate provides
the `kc` command.

## Install

```sh
cargo install kc-cli
```

## Keychain selection

Every command that accepts `--keychain` resolves keychains in this order:

1. explicit `--keychain KEYCHAIN` or a command's explicit keychain argument;
2. `KC_DEFAULT_KEYCHAIN`;
3. `keychains.default` in `~/.config/keychain.kdl`;
4. `login`.

Bare names are searched under `~/Library/Keychains` and then under configured
search paths. `machina` therefore resolves to
`~/Library/Keychains/machina.keychain-db`; `system` is the system keychain.
Paths containing `/` are treated as paths.

```sh
kc config set keychains.default machina
kc config set search.paths "path1" "path2"
kc config append search.paths "path3"
kc config prepend search.paths "preferred"
kc config show

export KC_DEFAULT_KEYCHAIN="$PWD/secrets.keychain-db"
```

`config show` reports both the saved default and the effective default with its
source.

## Create and add

Creation always requires an output path or name:

```sh
kc create ./secrets.keychain-db
kc create --password-gen --password-policy secure ./secrets.keychain-db
```

Password items use one assignment grammar:

```sh
kc add \
  class=generic \
  label=machina-vpn-operator \
  account=machina \
  service=cloudflare \
  kind="api key" \
  comment="a vpn operator" \
  --keychain ./secrets.keychain-db

kc add \
  class=internet \
  label="Gmail Account" \
  account=bryan@example.com \
  server=imap.gmail.com \
  port=443 \
  kind=password
```

Supported classes are `generic`, `internet`, and `appleshare`. Friendly field
names and native aliases are interchangeable: `account`/`acct`,
`service`/`svce`, `comment`/`icmt`, and so on. Class-invalid assignments are
errors.

When `-w, --secret` is absent, the item secret is read from stdin or prompted
for on a terminal.

Identities retain a purpose-built form because their inputs are files:

```sh
kc add identity -c certificate.pem -k private-key.der -l "MDM Push" login
kc add identity --pkcs12 identity.p12 login
kc import identity combined.pem login
```

Certificate and private-key inputs accept PEM or DER by content, regardless of
file extension. Private keys may be PKCS#8 or RSA PKCS#1. PKCS#12/PFX accepts
DER or PEM and has independent `--pkcs12-password*` inputs.

## Get

`get` is the only item read command. With no predicates it returns all
queryable password, certificate, public-key, private-key, and item-key records.

```sh
kc get
kc get class:internet label:"Gmail Account" kind:"api key"
kc get \
  'class:internet label:"Gmail Account" kind:"api key" icmt:%2026%'
kc get class:internet label:"Gmail Account" 'icmt:%2026%'
```

Predicates are ANDed. The canonical grammar is:

```text
FIELD:VALUE
FIELD:>=VALUE
FIELD[MODIFIERS]:PATTERN
```

Comparisons are `=`, `!=`, `<`, `<=`, `>`, and `>=`. A value containing `%` or
`_` uses SQL-LIKE semantics: `%` matches any sequence and `_` matches one
character. Backslash escapes a wildcard. `[c]` is case-insensitive, `[d]` is
diacritic-insensitive, and `[cd]` enables both.

Examples:

```sh
kc get \
  'cdat:<20260515074219Z' \
  'mdat:>=20250515074219Z' \
  'icmt:%2026%' \
  'label[cd]:com.%'
```

Dates accept Keychain UTC timestamps (`YYYYMMDDhhmmssZ`) and RFC 3339 UTC
timestamps. Numeric and boolean fields are compared as their actual types.
Unknown modifiers, malformed comparisons, invalid typed values, and unknown
exact class names are errors.

### Projection

`-o, --output` selects ordered fields:

```sh
kc get \
  class:generic account:machina service:vpn \
  -o label,kind,account,service

kc get class:internet -o '*'
kc --json get class:internet -o account,server,port
```

`-u, --distinct` emits each projected tuple once while preserving first-seen
order. Deduplication uses typed projected values before text or JSON rendering:

```sh
# Unique non-empty service values
kc get 'service:_%' -o service --distinct

# Unique account/service pairs
kc get 'service:_%' -o account,service -u
```

`service:%` also matches an empty service; `service:_%` requires at least one
character. The query language intentionally uses `%` rather than shell-glob
`*`.

`*` emits full record detail. `secret` decrypts the selected item. More than
one secret requires `--all`; non-secret queries intentionally allow multiple
rows.

```sh
kc get class:generic account:machina -o secret
kc --format secret get class:generic account:machina
kc get class:generic -o secret --all
```

`@ref` emits opaque, revision-bound item references for mutation pipelines:

```sh
kc get class:internet 'account[c]:bryan%' -o @ref |
  kc set label=updated --for -
```

References carry their keychain and database revision. `set` validates the
complete stream before changing memory and saves once. A malformed, duplicate,
wrong-keychain, missing, or stale reference aborts the operation without
writing. Human-readable and JSON output are never reparsed as selectors.

## Set

`set` applies assignments to every item matched by `--for`:

```sh
kc set label=updated comment="rotated in 2026" \
  --for 'class:internet account[c]:bryan%'
```

The query must be non-empty. `class`, `record`, `secret`, creation time, and
modification time cannot be changed. Attributes that define a relation's unique
identity are rejected by the library; replacing those means deleting and
adding an item.

Updates are collected first, applied in memory, and saved once. A failure
before the save leaves the file unchanged.

## Password input

All keychain-password consumers use the same sources:

```text
-E, --password-env [ENV_VAR]       default ENV_VAR: KC_PASSWORD
-F, --password-file FILE           use - for stdin
    --password-gen[=TEMPLATE]       creation/change only; default: sha1
-P, --password PASSWORD
    --password-policy POLICY        alphanumeric|mixed-case|symbol|secure
```

When no source is set, `kc` reads the password from stdin or prompts on a
terminal. Environment variables and files avoid exposing secrets in process
arguments. If `-P` is useful for shell composition, prefer a shell expression:

```sh
kc get class:generic account:alice -o secret \
  -P "$(secret-tool lookup kc login)"
```

## Access policy and native ACLs

New keychains default to a saved `hybrid` policy whose direct and native
decision is `prompt`. Direct secret access therefore requires `--interactive`;
native ACLs pre-authorize no caller, allowing `securityd` to show its normal
authorization UI.

```sh
kc access show machina
kc access set --mode hybrid --default prompt machina
kc access apply --to securityd machina
kc access audit --against securityd machina
```

`extended` enforces the `kc` policy only, `native` delegates to item ACLs, and
`hybrid` does both. Keychain-wide policy is projected onto items when they are
written and can be reapplied to existing items. Native Keychain ACLs are still
item-level data; the saved keychain policy is the template and enforcement
model for all items.

For individual legacy operations, `kc trust` supports:

```sh
kc trust -a alice -T /Applications/MyApp.app login
kc trust -a alice --trust-requirement \
  /Applications/MyApp.app=requirement.bin login
kc trust -a alice --prompt login
kc trust -a alice -A login
```

`--trust-requirement PATH=FILE` stores the compiled code requirement while the
legacy cdhash remains optional. This preserves Security.framework
interoperability while allowing stronger application identity.

## Identity import and export

```sh
kc export cert -l "MDM Push" -o certificate.pem login
kc export key -l "MDM Push" -o private-key.pem login
kc export identity -l "MDM Push" -o identity.pem login
kc export identity -l "MDM Push" --pkcs12 -o identity.p12 login
kc export identity -l "MDM Push" --pkcs12 --pem -o identity.p12.pem login
```

An explicit output path is recommended for exported secret material. PKCS#12
exports are encrypted with the selected PKCS#12 password.

## Other commands

```text
kc info [KEYCHAIN]
kc verify [KEYCHAIN]
kc rm item SELECTOR [--all] [KEYCHAIN]
kc rm identity [-l LABEL | --hash HEX] [KEYCHAIN]
kc cp SELECTOR SOURCE DESTINATION
kc passwd [KEYCHAIN]
kc settings [OPTIONS] [KEYCHAIN]
kc completions SHELL
```

`show`, `find`, and `ls` were removed in 0.6.0. Their behavior is represented
by `get`, projections, and an empty query.

## Output and exit status

Text is the default. `--json` is shorthand for `--format json`;
`--format secret` emits only secret bytes suitable for a pipeline.

Common exit statuses are stable:

```text
0   success
44  no item matched
45  incorrect keychain password
46  duplicate item
2   command-line or other operational error
```

## Development

```sh
cargo test --workspace --all-targets
cargo clippy --workspace --all-targets --all-features -- -D warnings
mise run verify
```

The release order is `keychain-rs` first, then `kc-cli`.

## License

MIT