Skip to content

Commit a5f279f

Browse files
sgx-labsclaude
andcommitted
fix: security hardening and doc accuracy for v0.12.0
- Add path traversal validation in recordProvenanceSources and CheckSourceDivergence - Add vault-staleness and vault-source-divergence to sanitizeContextTags - Sanitize divergence context output to prevent prompt injection via paths - Strip newlines from kaizen frontmatter values to prevent YAML injection - Update timing claims to realistic values in README, CHANGELOG, npm/README - Fix npm/README stale MCP tool count (12 → 17) and add missing tools Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 0231938 commit a5f279f

8 files changed

Lines changed: 35 additions & 11 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -470,7 +470,7 @@ Security hardening, crash recovery, search improvements, UX polish, and every ho
470470

471471
### Changed
472472

473-
- **README rewritten** — pain-first opening, architecture diagram, feature matrix, competitor comparison table, honest benchmarks (removed aspirational token claim)
473+
- **README rewritten** — pain-first opening, architecture diagram, feature matrix, competitor comparison table, updated benchmarks
474474
- **Schema v3** — adds `session_recovery` table; auto-migrates from v2
475475
- **MCP tool list updated** — setup now shows all 11 tools with accurate descriptions
476476

@@ -489,7 +489,7 @@ Self-diagnosing retrieval, pinned notes, keyword fallback, vault privacy structu
489489
- `get_session_context` — one-call orientation: pinned notes + latest handoff + recent activity + stats
490490
- `recent_activity` — recently modified notes (clamped to 50)
491491
- **`same ask`** — ask questions, get answers FROM your notes with source citations. Uses a local Ollama LLM to synthesize answers from semantically relevant notes. Auto-detects the best available chat model. 100% local, no cloud APIs. Example: `same ask "what did we decide about authentication?"`
492-
- **`same demo`** — interactive demo that creates a temporary vault with 6 realistic sample notes, indexes them, runs search, and showcases `same ask`. Works without Ollama (keyword-only mode). See SAME in action in under 60 seconds.
492+
- **`same demo`** — interactive demo that creates a temporary vault with 6 realistic sample notes, indexes them, runs search, and showcases `same ask`. Works without Ollama (keyword-only mode).
493493
- **`same tutorial`** — modular learn-by-doing system with 6 lessons: semantic search, decisions, pinning, privacy tiers, RAG chat, and session handoffs. Run all lessons (`same tutorial`) or jump to any topic (`same tutorial search`, `same tutorial pin`). Creates real notes and runs real commands — you learn the CLI by using it.
494494
- **SAME Lite (keyword-only mode)** — SAME now works without Ollama. When Ollama is unavailable, `same init` offers keyword-only mode using SQLite FTS5. All features work — search, ask, demo, tutorial — with keyword matching instead of semantic search. Install Ollama later and `same reindex` upgrades to full semantic mode. Zero dependencies beyond the binary.
495495
- **Project-aware init**`same init` now detects existing project documentation (README.md, docs/, ARCHITECTURE.md, CLAUDE.md, .cursorrules, ADR/) and offers to index them. Zero new notes required — your project already has context.

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Or via npm (all platforms): `npm install -g @sgx-labs/same`
2828

2929
**Installed via npm?** Update with `npx same@latest` or `npm update -g @sgx-labs/same`.
3030

31-
## See It Work (30 seconds)
31+
## See It Work
3232

3333
```bash
3434
same demo
@@ -127,7 +127,7 @@ Your markdown notes get embedded and stored in SQLite. When your AI starts a ses
127127
| MRR | **0.949** (right note first, almost every time) |
128128
| Prompt overhead | **<200ms** |
129129
| Binary size | **~12MB** |
130-
| Setup time | **<60 seconds** |
130+
| Setup time | **Under 2 minutes** |
131131

132132
## Add to Your AI Tool
133133

cmd/same/kaizen_cmd.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,9 +224,12 @@ func runKaizenAdd(description, agent, status string) error {
224224
}
225225

226226
// Build frontmatter
227+
// SECURITY: strip newlines from frontmatter values to prevent YAML injection
228+
safeTitle := strings.ReplaceAll(strings.ReplaceAll(description, "\n", " "), "\r", " ")
229+
227230
var content strings.Builder
228231
content.WriteString("---\n")
229-
content.WriteString(fmt.Sprintf("title: %s\n", description))
232+
content.WriteString(fmt.Sprintf("title: %s\n", safeTitle))
230233
content.WriteString("content_type: kaizen\n")
231234
content.WriteString(fmt.Sprintf("status: %s\n", status))
232235
if agent != "" {

internal/hooks/staleness_check.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@ func buildDivergenceContext(db *store.DB, vaultPath string) string {
7777
lines = append(lines, fmt.Sprintf("- %s (source: %s changed %s)", d.NotePath, sourceBase, agoStr))
7878
}
7979

80-
return strings.Join(lines, "\n")
80+
// SECURITY: sanitize output to prevent prompt injection via crafted paths
81+
return sanitizeContextTags(strings.Join(lines, "\n"))
8182
}
8283

8384
// formatDivergenceAge formats a duration as a relative time string for divergence context.

internal/hooks/text_processing.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,8 @@ func sanitizeContextTags(text string) string {
206206
"session-bootstrap",
207207
"vault-handoff",
208208
"vault-decisions",
209+
"vault-staleness",
210+
"vault-source-divergence",
209211
"same-diagnostic",
210212
// F16: Additional tags used by AI systems that could enable prompt injection
211213
"system-reminder",

internal/mcp/server.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -681,6 +681,10 @@ func recordProvenanceSources(notePath string, explicitSources []string) {
681681
if srcPath == "" {
682682
continue
683683
}
684+
// SECURITY: validate path to prevent traversal (e.g. "../../etc/passwd")
685+
if safeVaultPath(srcPath) == "" {
686+
continue
687+
}
684688
hash := ""
685689
fullPath := filepath.Join(vaultRoot, srcPath)
686690
if content, err := os.ReadFile(fullPath); err == nil {
@@ -1518,18 +1522,22 @@ func handleSaveKaizen(ctx context.Context, req *mcp.CallToolRequest, input saveK
15181522
}
15191523

15201524
// Build content
1525+
// SECURITY: strip newlines from frontmatter values to prevent YAML injection
1526+
safeTitle := strings.ReplaceAll(strings.ReplaceAll(description, "\n", " "), "\r", " ")
1527+
safeArea := strings.ReplaceAll(strings.ReplaceAll(strings.TrimSpace(input.Area), "\n", " "), "\r", " ")
1528+
15211529
var content strings.Builder
15221530
mcpHeader := "<!-- Note saved via SAME MCP tool. Review before trusting. -->\n"
15231531
content.WriteString(mcpHeader)
15241532
content.WriteString("---\n")
1525-
content.WriteString(fmt.Sprintf("title: %s\n", description))
1533+
content.WriteString(fmt.Sprintf("title: %s\n", safeTitle))
15261534
content.WriteString("content_type: kaizen\n")
15271535
content.WriteString("status: open\n")
15281536
if agent != "" {
15291537
content.WriteString(fmt.Sprintf("agent: %s\n", agent))
15301538
}
1531-
if input.Area != "" {
1532-
content.WriteString(fmt.Sprintf("area: %s\n", strings.TrimSpace(input.Area)))
1539+
if safeArea != "" {
1540+
content.WriteString(fmt.Sprintf("area: %s\n", safeArea))
15331541
}
15341542
content.WriteString("tags: [kaizen]\n")
15351543
content.WriteString("---\n\n")

internal/store/sources.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,11 @@ func (db *DB) CheckSourceDivergence(vaultPath string) ([]DivergenceResult, error
168168
return nil, fmt.Errorf("scan divergence row: %w", err)
169169
}
170170

171+
// SECURITY: validate stored path to prevent traversal reads outside vault
172+
clean := filepath.ToSlash(filepath.Clean(sourcePath))
173+
if strings.HasPrefix(clean, "..") || filepath.IsAbs(clean) || strings.Contains(clean, "\x00") {
174+
continue
175+
}
171176
fullPath := filepath.Join(vaultPath, sourcePath)
172177
content, err := os.ReadFile(fullPath)
173178
if err != nil {

npm/README.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ Your AI agent forgets everything between sessions. SAME fixes that. It indexes y
1111
- **Session handoffs** — your agent writes what it did, the next session picks up where it left off
1212
- **Decision log** — decisions are saved and surfaced automatically in future sessions
1313
- **`same ask`** — RAG chat over your vault with source citations
14-
- **`same demo`** — try it in 60 seconds, creates a sandbox, cleans up after
14+
- **`same demo`** — try it interactively, creates a sandbox, cleans up after
1515

16-
## 12 MCP Tools
16+
## 17 MCP Tools
1717

1818
| Tool | Type | Description |
1919
|------|------|-------------|
@@ -29,6 +29,11 @@ Your AI agent forgets everything between sessions. SAME fixes that. It indexes y
2929
| `save_note` | write | Create or update a note (optional `agent` attribution) |
3030
| `save_decision` | write | Log a project decision (optional `agent` attribution) |
3131
| `create_handoff` | write | Create a session handoff note (optional `agent` attribution) |
32+
| `save_kaizen` | write | Log improvement items with provenance tracking |
33+
| `mem_consolidate` | write | Consolidate related notes via LLM |
34+
| `mem_brief` | read | Generate orientation briefing |
35+
| `mem_health` | read | Vault health with trust analysis |
36+
| `mem_forget` | write | Suppress a note from search results |
3237

3338
## MCP Configuration
3439

0 commit comments

Comments
 (0)