Skip to content

Commit 3330f63

Browse files
sgx-labsclaude
andcommitted
Warn on unknown config keys, skip welcome notes in existing vaults
- Unknown TOML keys now print warnings with suggestions (e.g. "exclude_paths" → did you mean "skip_dirs"?) - Welcome notes skipped if vault already has markdown files - Fixes silent config failures that caused skip_dirs to not work Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a767f8e commit 3330f63

4 files changed

Lines changed: 116 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313

1414
### Fixed
1515

16+
- **Unknown config keys now warn** — unrecognized keys in `config.toml` print a warning with suggestions (e.g. `exclude_paths``skip_dirs`). Previously unknown keys were silently ignored.
17+
- **Welcome notes skip existing vaults**`same init` no longer creates `welcome/` in vaults that already have markdown files. Governed vaults with existing structure are left untouched.
1618
- **`SAME_EMBED_BASE_URL` missing from `LoadConfig()`** — the env var was handled in `EmbeddingProviderConfig()` but not in the config-file loader, causing inconsistency when both paths were used.
1719
- **`OPENAI_API_KEY` fallback for `openai-compatible`**`LoadConfig()` now checks the env var for both `openai` and `openai-compatible` providers.
1820
- **`BaseURL` not passed to embedding provider**`indexer.go` and `init.go` now pass the configured `BaseURL` when constructing embedding providers, fixing silent failures when using `openai-compatible` with a custom endpoint.

internal/config/config.go

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,9 +187,11 @@ func LoadConfig() (*Config, error) {
187187
// Try to load TOML config file
188188
configPath := findConfigFile()
189189
if configPath != "" {
190-
if _, err := toml.DecodeFile(configPath, cfg); err != nil {
190+
meta, err := toml.DecodeFile(configPath, cfg)
191+
if err != nil {
191192
return nil, fmt.Errorf("parse config %s: %w", configPath, err)
192193
}
194+
warnUnknownKeys(meta, configPath)
193195
}
194196

195197
// Environment variables override TOML values
@@ -259,9 +261,11 @@ func LoadConfigFrom(configPath string) (*Config, error) {
259261

260262
if configPath != "" {
261263
if _, err := os.Stat(configPath); err == nil {
262-
if _, err := toml.DecodeFile(configPath, cfg); err != nil {
264+
meta, err := toml.DecodeFile(configPath, cfg)
265+
if err != nil {
263266
return nil, fmt.Errorf("parse config %s: %w", configPath, err)
264267
}
268+
warnUnknownKeys(meta, configPath)
265269
}
266270
}
267271

@@ -563,6 +567,45 @@ func FindConfigFile() string {
563567
return findConfigFile()
564568
}
565569

570+
// configSuggestions maps common wrong keys to the correct TOML key name.
571+
var configSuggestions = map[string]string{
572+
"exclude_paths": "skip_dirs",
573+
"exclude_dirs": "skip_dirs",
574+
"skip_paths": "skip_dirs",
575+
"ignored_dirs": "skip_dirs",
576+
"ignore_dirs": "skip_dirs",
577+
"excludes": "skip_dirs",
578+
"noise": "noise_paths",
579+
"apikey": "api_key",
580+
"api-key": "api_key",
581+
"baseurl": "base_url",
582+
"base-url": "base_url",
583+
"token_budget": "max_token_budget",
584+
"budget": "max_token_budget",
585+
}
586+
587+
// warnUnknownKeys prints warnings for unrecognized config keys.
588+
func warnUnknownKeys(meta toml.MetaData, configPath string) {
589+
undecoded := meta.Undecoded()
590+
if len(undecoded) == 0 {
591+
return
592+
}
593+
594+
fname := filepath.Base(configPath)
595+
for _, key := range undecoded {
596+
keyStr := key.String()
597+
lastPart := key[len(key)-1]
598+
599+
if suggestion, ok := configSuggestions[lastPart]; ok {
600+
fmt.Fprintf(os.Stderr, "same: WARNING: unknown key %q in %s — did you mean %q?\n",
601+
keyStr, fname, suggestion)
602+
} else {
603+
fmt.Fprintf(os.Stderr, "same: WARNING: unknown key %q in %s (will be ignored)\n",
604+
keyStr, fname)
605+
}
606+
}
607+
}
608+
566609
// defaultSkipDirs are directories to skip during vault walks.
567610
// SECURITY: _PRIVATE contains client-sensitive content and must never be indexed
568611
// or auto-surfaced. Access to _PRIVATE requires explicit MCP tool calls.

internal/config/config_security_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,54 @@ func TestLoadConfig_AllEnvVars(t *testing.T) {
176176
}
177177
}
178178

179+
func TestLoadConfig_UnknownKeys(t *testing.T) {
180+
dir := t.TempDir()
181+
configDir := filepath.Join(dir, ".same")
182+
os.MkdirAll(configDir, 0o755)
183+
184+
// Config with unknown keys — should not error but should warn
185+
os.WriteFile(filepath.Join(configDir, "config.toml"),
186+
[]byte(`[vault]
187+
exclude_paths = ["_Raw", "Scratch"]
188+
path = "/home/user/notes"
189+
190+
[embedding]
191+
provider = "ollama"
192+
`), 0o644)
193+
194+
t.Setenv("VAULT_PATH", dir)
195+
VaultOverride = dir
196+
defer func() { VaultOverride = "" }()
197+
198+
cfg, err := LoadConfig()
199+
if err != nil {
200+
t.Fatalf("unknown keys should not cause error: %v", err)
201+
}
202+
// Valid keys should still be parsed (VAULT_PATH env overrides toml path, so check provider)
203+
if cfg.Embedding.Provider != "ollama" {
204+
t.Errorf("expected embedding provider to be parsed, got %q", cfg.Embedding.Provider)
205+
}
206+
}
207+
208+
func TestConfigSuggestions(t *testing.T) {
209+
// Verify the suggestions map has expected entries
210+
tests := []struct {
211+
wrong string
212+
correct string
213+
}{
214+
{"exclude_paths", "skip_dirs"},
215+
{"exclude_dirs", "skip_dirs"},
216+
{"skip_paths", "skip_dirs"},
217+
{"apikey", "api_key"},
218+
{"base-url", "base_url"},
219+
}
220+
for _, tt := range tests {
221+
if got, ok := configSuggestions[tt.wrong]; !ok || got != tt.correct {
222+
t.Errorf("configSuggestions[%q] = %q, want %q", tt.wrong, got, tt.correct)
223+
}
224+
}
225+
}
226+
179227
func TestLoadConfig_NoEnvVars(t *testing.T) {
180228
// Unset all SAME-related env vars
181229
t.Setenv("VAULT_PATH", "")

internal/setup/init.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -579,6 +579,12 @@ func copyWelcomeNotes(vaultPath string) {
579579
return
580580
}
581581

582+
// Skip welcome notes if the vault already has markdown content.
583+
// Governed vaults (with CLAUDE.md, README.md, etc.) don't need starter notes.
584+
if vaultHasNotes(vaultPath) {
585+
return
586+
}
587+
582588
// Create the directory
583589
if err := os.MkdirAll(destDir, 0o755); err != nil {
584590
// Silently skip if we can't create the directory
@@ -617,6 +623,21 @@ func copyWelcomeNotes(vaultPath string) {
617623
}
618624
}
619625

626+
// vaultHasNotes checks if the vault root already contains markdown files.
627+
// Used to skip welcome note generation for vaults with existing content.
628+
func vaultHasNotes(vaultPath string) bool {
629+
entries, err := os.ReadDir(vaultPath)
630+
if err != nil {
631+
return false
632+
}
633+
for _, e := range entries {
634+
if !e.IsDir() && strings.HasSuffix(e.Name(), ".md") {
635+
return true
636+
}
637+
}
638+
return false
639+
}
640+
620641
// detectVault finds or prompts for the vault path.
621642
func detectVault(autoAccept bool) (string, error) {
622643
cwd, err := os.Getwd()

0 commit comments

Comments
 (0)