1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
|
package cmd
import (
"fmt"
"os"
"os/exec"
"strings"
"envault/vault"
"github.com/spf13/cobra"
)
func init() {
rootCmd.AddCommand(newRunCmd())
}
func newRunCmd() *cobra.Command {
var useKeychain bool
var saveKeychain bool
cmd := &cobra.Command{
Use: "run -- <command> [args...]",
Short: "Run a command with secrets injected as environment variables",
Long: `Injects secrets into the environment and replaces the current process
with the given command. Works seamlessly with PyCharm, VS Code, and other IDEs.
Authentication priority:
1. Active session (envault open) — no prompt, recommended
2. macOS Keychain (--keychain flag) — no prompt
3. Password prompt (fallback) — always works
PyCharm setup:
Terminal: envault open (once, valid 8h)
Run Config: envault run -- python -m uvicorn main:app --reload`,
Example: ` envault run -- uvicorn main:app --reload
envault run -- python main.py
envault run --save-keychain -- echo "keychain saved"
envault run --keychain -- python -m uvicorn main:app --reload`,
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
var vars map[string]string
// Priority 1: active session (envault open)
if sv := trySession(); sv != nil {
vars = sv
} else if useKeychain || saveKeychain {
// Priority 2: macOS Keychain
password, err := vault.KeychainGet()
if err != nil {
fmt.Fprintln(os.Stderr, "No password in Keychain — prompting once.")
password, err = promptAndOpen()
if err != nil {
return err
}
if err := vault.KeychainSet(password); err != nil {
fmt.Fprintf(os.Stderr, "Warning: could not save to Keychain: %v\n", err)
} else {
fmt.Fprintln(os.Stderr, "Password saved to macOS Keychain.")
}
}
path, err := resolveVaultPath()
if err != nil {
return err
}
v, err := vault.Open(path, password)
if err != nil {
return err
}
vars = v.GetAll(resolveProject())
} else {
// Priority 3: password prompt
v, _, _, err := openVault()
if err != nil {
return err
}
vars = v.GetAll(resolveProject())
}
// Inject secrets into environment
for k, val := range vars {
os.Setenv(k, val)
}
// For terraform: also inject as TF_VAR_<key> (hyphens → underscores)
if len(args) > 0 && (args[0] == "terraform" || strings.HasSuffix(args[0], "/terraform")) {
for k, val := range vars {
tfKey := "TF_VAR_" + strings.ReplaceAll(strings.ToLower(k), "-", "_")
if os.Getenv(tfKey) == "" {
os.Setenv(tfKey, val)
}
}
}
// Find and exec the command (replaces current process on Unix)
bin, err := exec.LookPath(args[0])
if err != nil {
return fmt.Errorf("command not found: %s", args[0])
}
return replaceProcess(bin, args)
},
}
cmd.Flags().BoolVarP(&useKeychain, "keychain", "k", false, "read master password from macOS Keychain (no prompt)")
cmd.Flags().BoolVar(&saveKeychain, "save-keychain", false, "prompt once, save to macOS Keychain, then run")
// Stop flag parsing at the first non-flag argument so flags like --reload
// are passed through to the child command, not interpreted by ev.
cmd.Flags().SetInterspersed(false)
return cmd
}
func promptAndOpen() (string, error) {
path, err := resolveVaultPath()
if err != nil {
return "", err
}
if !vault.Exists(path) {
fmt.Fprintln(os.Stderr, "No vault found – creating a new one.")
p1, err := vault.PromptPassword("Set master password: ")
if err != nil {
return "", err
}
p2, err := vault.PromptPassword("Confirm master password: ")
if err != nil {
return "", err
}
if p1 != p2 {
return "", fmt.Errorf("passwords do not match")
}
return p1, nil
}
return vault.PromptPassword("Master password: ")
}
|