Skip to content

feat: read login password and key from env or stdin#3498

Open
bradleyfrank wants to merge 2 commits into
atuinsh:mainfrom
bradleyfrank:feat/login-env-and-stdin
Open

feat: read login password and key from env or stdin#3498
bradleyfrank wants to merge 2 commits into
atuinsh:mainfrom
bradleyfrank:feat/login-env-and-stdin

Conversation

@bradleyfrank

@bradleyfrank bradleyfrank commented May 25, 2026

Copy link
Copy Markdown

Resolves the unattended-login concern from #183: atuin login -p <password> exposes the password in the host's process list, which is unsafe in shared environments and impractical for non-interactive callers (CI, config management like Ansible, etc.). The 2021 fix (#185) added an interactive prompt, but that path is unavailable when there's no TTY.

This PR adds two non-interactive sources:

# Environment variable (also works for the encryption key)
ATUIN_PASSWORD=hunter2 ATUIN_ENCRYPTION_KEY=... atuin login -u <user>

# Read password from stdin (mutually exclusive with --password)
printf %s "$PASSWORD" | atuin login -u <user> --password-stdin

Precedence: --password flag → --password-stdinATUIN_PASSWORD env → interactive prompt. Key resolution follows the same shape minus stdin (--keyATUIN_ENCRYPTION_KEY env → interactive prompt). Same env+stdin treatment applied to atuin register. Existing flag-only and interactive flows are unchanged.

ATUIN_ENCRYPTION_KEY is named to avoid confusion with the ATUIN_KEY label that atuin doctor already prints for key_path (the key file location). The variable in this PR holds key contents; they're different concepts.

For atuin register, stdin/env resolution is deferred until the headless path is reachable (both --username and --email provided), so a piped secret isn't silently consumed before falling through to the OAuth flow.

Local validation

  • cargo test -p atuin --bin atuin — 220 passing (214 existing + 6 new in account::login::tests)
  • cargo clippy -p atuin -- -D warnings -D clippy::redundant_clone — clean
  • cargo fmt --check — clean

(Note: cargo clippy -p atuin --tests reports 10 errors on main before my changes — they're in search/interactive.rs and keybindings/key.rs, unrelated to this PR.)

Notes

AI-assisted (Claude) and human-reviewed/tested before submission. Implementation reads the existing or_user_input / read_user_password pattern and adds parallel helpers (env_secret, read_secret_from_stdin) in the same pub(super) style so register.rs can reuse them.

Checks

  • I am happy for maintainers to push small adjustments to this PR, to speed up the review cycle
  • I have checked that there are no existing pull requests for the same thing

Adds ATUIN_PASSWORD and ATUIN_KEY environment variable fallbacks and a
new --password-stdin flag to `atuin login`, with the same env+stdin
treatment for `atuin register`. Addresses the unattended-login gap
from atuinsh#183: --password is visible in the host's process list, which is
unsafe in shared environments and impractical for CI / config
management (Ansible, etc.) workflows. The 2021 fix (atuinsh#185) only added
an interactive prompt, which doesn't help non-interactive callers.

Precedence: --password flag > --password-stdin > env var > interactive
prompt. --password and --password-stdin are mutually exclusive
(enforced by clap). Existing flag-only and interactive flows are
unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented May 25, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds non-interactive password and key sources for login and registration. The main changes are:

  • Adds --password-stdin for login and register.
  • Adds ATUIN_PASSWORD fallback for account passwords.
  • Adds ATUIN_KEY fallback for login encryption key input.
  • Reuses the new secret helpers across login and register flows.

Confidence Score: 3/5

These issues should be fixed before merging.

  • Existing ATUIN_KEY key-path overrides can be misread as encryption key contents.
  • Hub registration can consume or block on stdin before it knows the headless path is usable.
  • The changed code is limited to account login and registration flows.

Focus on login.rs env naming and register.rs stdin read order.

Important Files Changed

Filename Overview
crates/atuin/src/command/client/account/login.rs Adds password/key env fallback and stdin password handling for login.
crates/atuin/src/command/client/account/register.rs Adds password env/stdin resolution for Hub and legacy registration.

Reviews (1): Last reviewed commit: "feat: read login password and key from e..." | Re-trigger Greptile

Comment on lines +229 to +233
let key = self
.key
.clone()
.or_else(|| env_secret(KEY_ENV))
.unwrap_or_else(|| read_user_input("encryption key [blank to use existing key file]"));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Avoid ATUIN_KEY collision

ATUIN_KEY is already used by Atuin's config environment loader as the key_path override, and doctor.rs reports that same variable as the key path. This new fallback now treats the same value as raw encryption key material. When a user has ATUIN_KEY=/custom/path/to/key to point Atuin at a key file, atuin login will try to parse /custom/path/to/key as the encryption key and fail instead of using the existing key file.

SyncAuth::NotLoggedIn { .. } => {}
}

let resolved_password = self.resolve_password()?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Delay stdin password read

resolve_password() reads all of stdin when --password-stdin is set, but this happens before Hub registration checks whether username, email, and password are all present for the headless path. For example, printf %s "$PASSWORD" | atuin register --password-stdin --email a@b.com consumes the piped secret and then falls back to browser registration because username is missing. With terminal stdin, this can also block before the fallback message is reached.

* Rename ATUIN_KEY -> ATUIN_ENCRYPTION_KEY. The `atuin doctor` output
  already labels the `key_path` setting with "ATUIN_KEY", so reusing
  that name for a different concept (raw key contents) would have been
  user-facing confusion even though figment's prefix wiring does not
  actually collide (the real path override would be ATUIN_KEY_PATH).
* Defer register's stdin/env password read until the headless path is
  reachable, so `printf %s "$PASSWORD" | atuin register --password-stdin
  --email a@b.com` (no username) no longer consumes the piped secret
  before falling through to OAuth.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bradleyfrank

bradleyfrank commented May 25, 2026

Copy link
Copy Markdown
Author

Thanks for the review (and to Greptile for the catches). Pushed c40b229 addressing both P1s:

  1. ATUIN_KEY collision — renamed to ATUIN_ENCRYPTION_KEY. On verification, figment's prefix wiring (Environment::with_prefix("atuin").prefix_separator("_").separator("__")) means there's no literal runtime collision (the path override would actually be ATUIN_KEY_PATH, not ATUIN_KEY). But doctor.rs:307 already labels the key_path setting as ATUIN_KEY in user-facing output, so reusing that name for raw key contents would be confusing even without a hard collision. The new name is also more accurate — what this variable carries is the encryption key, not a key file path.

    Drive-by observation (didn't fix here to keep scope focused): the ("ATUIN_KEY", &self.key) label in doctor.rs is itself misleading since ATUIN_KEY isn't actually wired to anything in figment — the real path override is ATUIN_KEY_PATH.

  2. Stdin consumed before headless check in register — deferred. The password resolution now only runs in the hub branch when both --username and --email are present (i.e., when the headless path is actually reachable). Legacy registration still resolves eagerly since it always needs a password. So printf %s "$PASSWORD" | atuin register --password-stdin --email a@b.com (no username) no longer touches stdin before falling through to OAuth.

Tests, clippy, fmt all clean locally.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant