Skip to content

Add import --interactive and --empty modes for .env templates - #2

Open
peteraba wants to merge 1 commit into
mainfrom
import-interactive-empty
Open

Add import --interactive and --empty modes for .env templates#2
peteraba wants to merge 1 commit into
mainfrom
import-interactive-empty

Conversation

@peteraba

@peteraba peteraba commented Jun 11, 2026

Copy link
Copy Markdown
Owner

Summary

Projects often ship a .env.example listing required variables with defaults where they exist and blanks where they don't. Two new modes on import turn such a template into stored entries:

  • import -i FILE walks the user through a huh TUI form, one field per variable, template values pre-filled as defaults. Secret-looking names (KEY/SECRET/TOKEN/PASSWORD/PASS) are masked while typing. The form renders to stderr so it stays visible under the shell-init wrapper's command substitution.
  • import --empty [FILE] stores an empty value for every name, ignoring template defaults, for non-interactive scaffolding.

The encrypt+store loop is extracted from cmdImport into storeAll, and import is added to the shell wrapper passthrough lists since it never emits eval-able stdout.

Implemented via Ultraplan cloud session: https://claude.ai/code/session_01AK32fkpw3G7zkFERembrtf

Test plan

  • go build ./... and go test ./... pass (new CLI tests cover --empty, error cases, and template parsing)
  • Manual check of the interactive form: envmagic import -i .env.magic in a scratch project, verifying defaults pre-fill, secret masking, and stored values via envmagic export

🤖 Generated with Claude Code

Greptile Summary

This PR adds two new modes to the import command: --interactive (-i) renders a huh TUI form pre-filled with template defaults so the user can fill in secrets one-by-one, and --empty scaffolds a namespace by storing every parsed name with an empty value. The storeAll helper is extracted from cmdImport, import is added to shell-wrapper passthrough lists, and charmbracelet/huh plus its bubbletea dependency tree are added.

  • form.go introduces promptForValues and looksSecret; secret-looking fields (names containing KEY, SECRET, TOKEN, PASSWORD, PASS) render in password-echo mode while the form renders to stderr to stay visible under the shell wrapper's command substitution.
  • envfile.go refactors cmdImport to branch on --empty/--interactive before calling the shared storeAll path; shellinit.go adds import to the POSIX and fish passthrough case lists.

Confidence Score: 3/5

The core import logic and shell passthrough changes are correct, but the interactive form leaks existing secret values in plaintext in the description widget — the masking only applies to the input box, not the pre-filled description label.

The secret-value leak in the form description is a real current defect on the interactive import path: any user with an existing .env file (not just a blank template) would see their passwords and tokens in clear text on screen. The fix is straightforward but must land before the feature ships.

cmd/envmagic/form.go — the description-label logic in promptForValues needs to hide the raw value for secret-looking fields.

Security Review

  • Secret value exposed in form description (cmd/envmagic/form.go, promptForValues): when a secret-looking field has a non-empty value, the description renders \"default: <raw_value>\" in plaintext even though the input widget is in EchoModePassword. This leaks the existing secret in clear text on the terminal for anyone with line-of-sight or a screen recording.

Important Files Changed

Filename Overview
cmd/envmagic/form.go New file implementing TUI form for interactive import; description text reveals template value in plaintext for masked fields, and looksSecret has an unreachable PASSWORD marker.
cmd/envmagic/envfile.go cmdImport correctly branches on --empty/--interactive and storeAll extraction is clean.
cmd/envmagic/main.go New --interactive and --empty flags cleanly registered on the import subcommand.
cmd/envmagic/shellinit.go import correctly added to passthrough case lists in both POSIX and fish shell wrappers.
cmd/envmagic/cli_test.go New TestImportTemplate covers --empty, mutual exclusion, missing FILE, no-TTY error hint, and malformed input.
go.mod Adds charmbracelet/huh v1.0.0 and go-isatty; dependency tree is consistent with huh/bubbletea usage.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[envmagic import] --> B{flags?}
    B -- "--interactive AND --empty" --> ERR1[Exit 2: mutually exclusive]
    B -- "--interactive, no FILE" --> ERR2[Exit 2: FILE required]
    B -- "--interactive FILE" --> C{isatty stdin?}
    C -- No --> ERR3[Exit 1: requires terminal]
    C -- Yes --> D[promptForValues via huh TUI form to stderr]
    D -- user aborts --> ERR4[Exit 1: aborted]
    D -- user submits --> E[storeAll]
    B -- "--empty" --> F[zero-out all values]
    F --> E
    B -- plain import --> G[parseDotenv]
    G --> E
    E --> H[findOrCreateStorePath + Encrypt + Set each kv]
    H --> I[stderr confirmation]
Loading

Fix All in Cursor

Reviews (1): Last reviewed commit: "Add import --interactive and --empty mod..." | Re-trigger Greptile

Greptile also left 2 inline comments on this PR.

Projects often ship a .env.example listing required variables with
defaults where they exist and blanks where they don't. Two new modes on
`import` turn such a template into stored entries:

- `import -i FILE` walks the user through a huh TUI form, one field per
  variable, template values pre-filled as defaults. Secret-looking names
  (KEY/SECRET/TOKEN/PASSWORD/PASS) are masked while typing. The form
  renders to stderr so it stays visible under the shell-init wrapper's
  command substitution.
- `import --empty [FILE]` stores an empty value for every name,
  ignoring template defaults, for non-interactive scaffolding.

The encrypt+store loop is extracted from cmdImport into storeAll, and
`import` is added to the shell wrapper passthrough lists since it never
emits eval-able stdout.

https://claude.ai/code/session_01AK32fkpw3G7zkFERembrtf

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Security review complete. Found 1 medium issue: interactive imports render dotenv defaults to the terminal before encryption, which can disclose secret defaults despite password masking. No prior review threads were present, and govulncheck reported no reachable vulnerabilities in imported packages.

Open in Web View Automation 

Sent by Cursor Automation: Find vulnerabilities

Comment thread cmd/envmagic/form.go
for i := range kvs {
desc := "(no default)"
if kvs[i][1] != "" {
desc = "default: " + kvs[i][1]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Severity: Medium. kvs[i][1] comes from the imported dotenv file and is rendered to stderr as default: ...; for names like API_TOKEN the input is later password-masked, but this description still displays the secret default in clear. Importing a real .env or a template containing credentials leaks them to terminal scrollback/logs before encryption. Redact or omit defaults for looksSecret fields.

Comment thread cmd/envmagic/form.go
Comment on lines +26 to +36
desc := "(no default)"
if kvs[i][1] != "" {
desc = "default: " + kvs[i][1]
}
input := huh.NewInput().
Title(kvs[i][0]).
Description(desc).
Value(&kvs[i][1])
if looksSecret(kvs[i][0]) {
input = input.EchoMode(huh.EchoModePassword)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 security The description label renders the raw template value in plaintext — "default: my-real-secret" — even when the field is in EchoModePassword. A user importing a real .env file (rather than a blank .env.example) would see the existing secret value in clear text in the form description, which defeats the masking. Show a placeholder instead of the actual value for secret-looking fields.

Suggested change
desc := "(no default)"
if kvs[i][1] != "" {
desc = "default: " + kvs[i][1]
}
input := huh.NewInput().
Title(kvs[i][0]).
Description(desc).
Value(&kvs[i][1])
if looksSecret(kvs[i][0]) {
input = input.EchoMode(huh.EchoModePassword)
}
isSecret := looksSecret(kvs[i][0])
desc := "(no default)"
if kvs[i][1] != "" {
if isSecret {
desc = "(default set)"
} else {
desc = "default: " + kvs[i][1]
}
}
input := huh.NewInput().
Title(kvs[i][0]).
Description(desc).
Value(&kvs[i][1])
if isSecret {
input = input.EchoMode(huh.EchoModePassword)
}

Fix in Cursor

Comment thread cmd/envmagic/form.go
// looksSecret reports whether the (uppercase) variable name suggests a secret
// whose input should be masked.
func looksSecret(name string) bool {
for _, marker := range []string{"KEY", "SECRET", "TOKEN", "PASSWORD", "PASS"} {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 "PASSWORD" is unreachable — any name containing "PASSWORD" already contains "PASS", so the loop returns true before it ever reaches the "PASSWORD" entry. Remove the redundant marker.

Suggested change
for _, marker := range []string{"KEY", "SECRET", "TOKEN", "PASSWORD", "PASS"} {
for _, marker := range []string{"KEY", "SECRET", "TOKEN", "PASS"} {

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Cursor

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