Skip to content

Commit 92b848d

Browse files
committed
feat(config): load ~/.odek/secrets.env into process environment on startup
Add loadSecretsEnv() to LoadConfig as Layer 0 — runs before config files and env var lookups. Parses ~/.odek/secrets.env and calls os.Setenv for each KEY=VALUE pair. This eliminates the need to store secrets in config.json: - Keys stay in secrets.env (0600 permissions) - config.json can reference them via substitution - ODEK_* env var layer and legacy DEEPSEEK_API_KEY fallback still work as before - Missing or unreadable secrets.env is silently ignored
1 parent 0235374 commit 92b848d

1 file changed

Lines changed: 49 additions & 0 deletions

File tree

internal/config/loader.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@
1212
package config
1313

1414
import (
15+
"bufio"
1516
"encoding/json"
1617
"fmt"
1718
"os"
1819
"path/filepath"
1920
"strconv"
21+
"strings"
2022

2123
"github.com/BackendStack21/kode/internal/danger"
2224
"github.com/BackendStack21/kode/internal/mcpclient"
@@ -367,6 +369,11 @@ func envInt(key string) int {
367369
// API key has an additional fallback: if none of the four layers provides
368370
// one, it falls back to DEEPSEEK_API_KEY → OPENAI_API_KEY (legacy env vars).
369371
func LoadConfig(cli CLIFlags) ResolvedConfig {
372+
// Layer 0: load ~/.odek/secrets.env into the process environment.
373+
// This makes secrets available as env vars for ${VAR} substitution
374+
// in config files and for ODEK_* env var lookups.
375+
loadSecretsEnv()
376+
370377
// Layer 1: global (~/.odek/config.json)
371378
global := loadFile(GlobalConfigPath())
372379

@@ -849,3 +856,45 @@ func overlayFile(base, override FileConfig) FileConfig {
849856
}
850857
return base
851858
}
859+
860+
// secretsEnvPath returns the path to the secrets environment file.
861+
func secretsEnvPath() string {
862+
home, err := os.UserHomeDir()
863+
if err != nil {
864+
return ""
865+
}
866+
return filepath.Join(home, ".odek", "secrets.env")
867+
}
868+
869+
// loadSecretsEnv reads ~/.odek/secrets.env and injects each KEY=VALUE pair
870+
// into the process environment via os.Setenv. This makes secrets available
871+
// for ${VAR} substitution in config files and for ODEK_* env var lookups.
872+
//
873+
// Missing or unreadable files are silently ignored — not an error.
874+
// Lines that don't match KEY=VALUE are silently skipped.
875+
func loadSecretsEnv() {
876+
path := secretsEnvPath()
877+
if path == "" {
878+
return
879+
}
880+
f, err := os.Open(path)
881+
if err != nil {
882+
return
883+
}
884+
defer f.Close()
885+
886+
scanner := bufio.NewScanner(f)
887+
for scanner.Scan() {
888+
line := strings.TrimSpace(scanner.Text())
889+
if line == "" || strings.HasPrefix(line, "#") {
890+
continue
891+
}
892+
k, v, ok := strings.Cut(line, "=")
893+
if !ok || k == "" {
894+
continue
895+
}
896+
if os.Getenv(k) == "" {
897+
os.Setenv(k, v)
898+
}
899+
}
900+
}

0 commit comments

Comments
 (0)