# Selected SSH import

`dreamlake vault import --ssh` reads a local SSH config and saves selected items through
the existing vault entry API. It does not connect to hosts. The destination server
must have authenticated vault routes and encryption configured; unavailable routes
are reported as failures. No episode or other plaintext upload is used.

```shell
# Discover item IDs and review metadata without reading private keys or calling vault.
dreamlake vault import --ssh -p alice --config /absolute/path/to/config --dry-run

# Open an empty checklist, then review selected destinations before uploading.
dreamlake vault import --ssh -p alice --config /absolute/path/to/config
```

Omitting `--config` uses `~/.ssh/config`. Use arrows to move, Space to toggle,
Enter to review, and Escape or Ctrl-C to cancel. The final upload confirmation
defaults to No. Empty selection or cancellation before upload writes nothing.

Profiles and private-key exports are separate rows. Selecting `profile:dev` keeps
its key-file and jump-host references but does not upload those keys, another
alias, or jump-host credentials. A key row is a candidate file reference, not a
promise that the file is exportable. Only selected key files are read, after
confirmation. Hardware-backed keys remain references.

```shell
# Automation: explicit selections are the upload consent; no prompt opens.
dreamlake vault import --ssh -p alice --config /absolute/path/to/config --json \
  --select profile:dev

# Add the key only when exporting its private contents is intended.
dreamlake vault import --ssh -p alice --config /absolute/path/to/config --json \
  --select profile:dev --select key:dev:1
```

`--select` is repeatable. Non-TTY or `--json` uploads require at least one explicit
selection. `--dry-run` can list all discovered metadata without a selection.
The review goes to stderr; stdout contains one JSON result with no secret values.
Neither a generic `--yes` nor quiet mode authorizes key export.

## Destinations and conflicts

For prefix `alice`, `profile:dev` maps to `alice/ssh/profiles/dev` and `key:dev:1`
maps to `alice/ssh/keys/dev-1`. Key indexes are the one-based `IdentityFile` order
within that concrete Host block. Review them again after editing the config.
The prefix is required; the review shows the server, prefix, item type and exact
vault names. Authentication is pinned for the upload/retry session.

Profiles are string-field maps with schema `dreamlake-ssh-profile-v1`, the alias,
explicit hostname/user/port, optional jump reference, and a JSON string containing
identity-file references. They contain no executable SSH directives. Omitted
user/port remain unspecified; global defaults are not guessed. Keys use the
ordinary vault string type, preserving UTF-8 bytes, CRLF and trailing newlines.
No local key file is written and no suggested filename triggers file export.

An equal active destination is reported as verified, without another write.
A differing destination is a conflict until its revision is explicitly supplied:

```shell
dreamlake vault show -n alice/ssh/profiles/dev
dreamlake vault import --ssh -p alice --config /absolute/path/to/config --json \
  --select profile:dev --if-match profile:dev=3
```

`--if-match` is repeatable and applies only to its selected item. Revisions are
never automatically advanced on retry. A missing revision means create-only.
Source files and unselected vault entries are not deleted or modified.

## Partial results and retries

Each selected item reports `pending`, `success`, `failure`, or `unknown`. Cancel
during upload to stop subsequent entries; an in-flight request is allowed to
settle so its outcome can be reported. Completed writes remain saved. There is
no batch rollback. Non-successful final results exit with status 1.

The interactive retry keeps the same selection and source values. Automation can
use `--retry 1` (maximum 3). Successful entries are not rewritten. Failed requests
keep their original revision guard. Uncertain writes are **only read back**: an
equal destination is verified, while an absent, different or unreadable destination
remains unknown. A transport failure or malformed write acknowledgement does not
prove that nothing was saved.

The backend currently has no entry-write operation receipt. Import cannot prove
which request produced an equal value, or safely replay an unresolved write.
Retry state is held in the current process, not a durable journal. Preserve the
redacted result and resolve unknown outcomes before starting a new invocation;
a new invocation is a new explicit request. No cross-process idempotency or
backend source-manifest support is claimed. A rejected source file needs a new
review after it is corrected.

## Pass OTP import and code retrieval

Choose one source: `--ssh` or `--pass-otp`. Redacted dry-run previews an explicitly
named store. Selected TOTP upload and owner-only code retrieval are available in CLI
`0.13.0` and Python `0.10.0`, with the deployed DreamLake API. For self-hosted
servers, use a backend with TOTP support. HOTP remains preview-only. Ordinary `--pass` is reserved and rejected.

```shell
dreamlake vault import --pass-otp -p alice --dry-run \
  --store /absolute/path/to/password-store --gpg-home /absolute/path/to/gnupg
```

The dry-run result contains metadata and `uploaded: false`; decryption failures and
invalid OTP records cause a nonzero exit. Source selection is mutually exclusive,
and source-specific options cannot be mixed. Prefix placement works before
`vault`, after `vault`, or after `import`; conflicting prefixes fail.

Existing `vault ssh sync` and `vault pass sync --otp` commands remain compatibility
aliases. Prefer `vault import --ssh` and `vault import --pass-otp` in new scripts.
Import is a selected, one-way operation: it does not delete source entries or
perform tracked reconciliation. The word `sync` is reserved for that future
capability rather than a promise made by the compatibility aliases.

## Supported sources

Discovery parses text without running OpenSSH, commands, plugins or config
conditions. `Include` is reported and not followed. `Match`, global defaults,
wildcard inheritance and blocks containing wildcard/negated patterns are not
resolved. Unsupported directives are omitted with line-number warnings, including
`ProxyCommand`, `LocalCommand` and host-key-checking overrides. The result is an
explicit-fields profile, not a fully evaluated OpenSSH configuration.

Concrete aliases must start with a letter/digit and contain only letters, digits,
underscores or hyphens, up to 128 characters. Duplicate aliases (including case
variants), dotted aliases and unsafe aliases are excluded with warnings; they
are never silently renamed. Simplify the selected config explicitly if necessary.
Quoted values, comments, CRLF and `Keyword=value` are supported. Ambiguous escaped
syntax or unsupported connection-field values exclude the affected profile.

Only absolute and `~/` key paths can offer export rows. Relative, tokenized,
wildcard, environment-expanded and `none` identity paths stay references.
Private-key files must be owned by the current user with no group/other permission
bits, and use a supported PEM/OpenSSH private-key envelope. Config files must not
be group/other writable. Both must be regular files, at most 64 KiB, valid UTF-8
without NULs, with no symlink components or hard links. File identity and changes
are checked while reading; serialized vault values must also fit 64 KiB.

Implementation tracking: [SSH sync #241](https://github.com/dreamlake-ai/dreamlake-workspace/issues/241)
and [master #247](https://github.com/dreamlake-ai/dreamlake-workspace/issues/247).
CLI `0.13.0` and Python `0.10.0` passed production import and TOTP checks; see the
[release evidence](https://docs.dreamlake.ai/dev/notes/vault-runtime/).
After importing credentials, verify the intended host login separately.

```shell
# Create only the selected login.gpg registration under alice/otp/login.
dreamlake vault import --pass-otp -p alice/otp --store /canonical/test-store --select login
# Explicitly reveal a code; never put it in logs or command arguments.
dreamlake vault otp -p alice/otp -n login
dreamlake vault otp -p alice/otp -n login --to-json
```

```python
result = client.vault.import_entries(source="pass-otp", prefix="alice/otp",
                                    store="/canonical/test-store", select=["login"])
code = client.vault.otp("login", prefix="alice/otp")
code_json = client.vault.otp("login", prefix="alice/otp", to_json=True)
```

Selection is mandatory, even on a TTY; interactive upload additionally asks
`[y/N]`. `--json` is noninteractive, and explicit `--select` consents to those
paths only. Python never prompts. Pass paths omit `.gpg` and use canonical
slash-separated names. GPG uses batch/no-tty/pinentry-error. Apply decrypts only
selected files, rejects HOTP, and excludes adjacent password lines. It validates
the entire batch and rechecks encrypted source bytes before upload. There is no
persisted preview plan; apply validates a fresh selection each time.

TOTP import creates entries only. Conflicts stop the batch; OTP does not
accept SSH `--if-match` or `--retry` options. Successful earlier writes are
retained. An `unknown` result means a write may have committed; inspect `show`
and securely compare with explicit `get` before deciding what to do. Never
blindly repeat an unknown import. General recovery is a separate workstream.

`otp --to-json` returns only `code` and `validUntil`; without it, output is the
code. The server clock determines the code, and entry expiry may shorten its
validity. Scoped keys cannot generate codes. Explicit `vault get` instead
reveals the registration JSON string, including its seed. The target login
service enforces one-time acceptance. HOTP authority/counter safety is deferred;
no HOTP counter is advanced by these commands. The legacy pass sync command
continues to provide preview only.
