Add import --interactive and --empty modes for .env templates - #2
Add import --interactive and --empty modes for .env templates#2peteraba wants to merge 1 commit into
Conversation
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
There was a problem hiding this comment.
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.
Sent by Cursor Automation: Find vulnerabilities
| for i := range kvs { | ||
| desc := "(no default)" | ||
| if kvs[i][1] != "" { | ||
| desc = "default: " + kvs[i][1] |
There was a problem hiding this comment.
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.
| 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) | ||
| } |
There was a problem hiding this comment.
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.
| 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) | |
| } |
| // 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"} { |
There was a problem hiding this comment.
"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.
| 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!


Summary
Projects often ship a
.env.examplelisting required variables with defaults where they exist and blanks where they don't. Two new modes onimportturn such a template into stored entries:import -i FILEwalks 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
cmdImportintostoreAll, andimportis 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 ./...andgo test ./...pass (new CLI tests cover--empty, error cases, and template parsing)envmagic import -i .env.magicin a scratch project, verifying defaults pre-fill, secret masking, and stored values viaenvmagic export🤖 Generated with Claude Code
Greptile Summary
This PR adds two new modes to the
importcommand:--interactive(-i) renders ahuhTUI form pre-filled with template defaults so the user can fill in secrets one-by-one, and--emptyscaffolds a namespace by storing every parsed name with an empty value. ThestoreAllhelper is extracted fromcmdImport,importis added to shell-wrapper passthrough lists, andcharmbracelet/huhplus its bubbletea dependency tree are added.form.gointroducespromptForValuesandlooksSecret; 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.gorefactorscmdImportto branch on--empty/--interactivebefore calling the sharedstoreAllpath;shellinit.goaddsimportto 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
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 inEchoModePassword. This leaks the existing secret in clear text on the terminal for anyone with line-of-sight or a screen recording.Important Files Changed
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]Reviews (1): Last reviewed commit: "Add import --interactive and --empty mod..." | Re-trigger Greptile