eagle-mem 3.0.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,156 @@
1
+ -- Migration 015: Data integrity fixes
2
+ -- 1. Add CHECK constraints for status fields (claude_tasks, features, command_rules)
3
+ -- 2. Add UNIQUE constraint to feature_smoke_tests to prevent duplicates
4
+ -- 3. Deduplicate any existing smoke test rows before adding constraint
5
+
6
+ -- ─── 1. CHECK constraints via table rebuild ──────────────────
7
+
8
+ -- claude_tasks: enforce status values
9
+ CREATE TABLE claude_tasks_new (
10
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
11
+ project TEXT NOT NULL DEFAULT '',
12
+ source_session_id TEXT NOT NULL,
13
+ source_task_id TEXT NOT NULL,
14
+ file_path TEXT UNIQUE,
15
+ subject TEXT,
16
+ description TEXT,
17
+ active_form TEXT,
18
+ status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'in_progress', 'completed', 'cancelled')),
19
+ blocks TEXT DEFAULT '[]',
20
+ blocked_by TEXT DEFAULT '[]',
21
+ content_hash TEXT,
22
+ captured_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
23
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
24
+ );
25
+
26
+ INSERT INTO claude_tasks_new SELECT * FROM claude_tasks;
27
+ DROP TABLE claude_tasks;
28
+ ALTER TABLE claude_tasks_new RENAME TO claude_tasks;
29
+
30
+ CREATE INDEX IF NOT EXISTS idx_claude_tasks_project ON claude_tasks(project);
31
+ CREATE INDEX IF NOT EXISTS idx_claude_tasks_session ON claude_tasks(source_session_id);
32
+ CREATE INDEX IF NOT EXISTS idx_claude_tasks_status ON claude_tasks(status);
33
+
34
+ -- Recreate FTS triggers (dropped with table)
35
+ CREATE TRIGGER claude_tasks_ai AFTER INSERT ON claude_tasks BEGIN
36
+ INSERT INTO claude_tasks_fts(rowid, subject, description)
37
+ VALUES (new.id, new.subject, new.description);
38
+ END;
39
+
40
+ CREATE TRIGGER claude_tasks_ad AFTER DELETE ON claude_tasks BEGIN
41
+ INSERT INTO claude_tasks_fts(claude_tasks_fts, rowid, subject, description)
42
+ VALUES ('delete', old.id, old.subject, old.description);
43
+ END;
44
+
45
+ CREATE TRIGGER claude_tasks_au AFTER UPDATE ON claude_tasks BEGIN
46
+ INSERT INTO claude_tasks_fts(claude_tasks_fts, rowid, subject, description)
47
+ VALUES ('delete', old.id, old.subject, old.description);
48
+ INSERT INTO claude_tasks_fts(rowid, subject, description)
49
+ VALUES (new.id, new.subject, new.description);
50
+ END;
51
+
52
+ -- features: enforce status values
53
+ CREATE TABLE features_new (
54
+ id INTEGER PRIMARY KEY,
55
+ project TEXT NOT NULL,
56
+ name TEXT NOT NULL,
57
+ description TEXT,
58
+ status TEXT DEFAULT 'active' CHECK (status IN ('active', 'archived', 'deprecated')),
59
+ last_verified_at TIMESTAMP,
60
+ last_verified_notes TEXT,
61
+ created_at TIMESTAMP DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
62
+ updated_at TIMESTAMP DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
63
+ UNIQUE(project, name)
64
+ );
65
+
66
+ INSERT INTO features_new SELECT * FROM features;
67
+ DROP TABLE features;
68
+ ALTER TABLE features_new RENAME TO features;
69
+
70
+ -- Recreate FTS triggers for features (dropped with table)
71
+ CREATE TRIGGER features_ai AFTER INSERT ON features BEGIN
72
+ INSERT INTO features_fts(rowid, name, description)
73
+ VALUES (new.id, new.name, new.description);
74
+ END;
75
+
76
+ CREATE TRIGGER features_ad AFTER DELETE ON features BEGIN
77
+ INSERT INTO features_fts(features_fts, rowid, name, description)
78
+ VALUES ('delete', old.id, old.name, old.description);
79
+ END;
80
+
81
+ CREATE TRIGGER features_au AFTER UPDATE ON features BEGIN
82
+ INSERT INTO features_fts(features_fts, rowid, name, description)
83
+ VALUES ('delete', old.id, old.name, old.description);
84
+ INSERT INTO features_fts(rowid, name, description)
85
+ VALUES (new.id, new.name, new.description);
86
+ END;
87
+
88
+ -- Rebuild child tables with CASCADE FKs (they reference features.id)
89
+ -- feature_dependencies: recreate to pick up new parent table
90
+ CREATE TABLE feature_dependencies_new (
91
+ id INTEGER PRIMARY KEY,
92
+ feature_id INTEGER NOT NULL,
93
+ kind TEXT NOT NULL,
94
+ target TEXT NOT NULL,
95
+ name TEXT NOT NULL,
96
+ notes TEXT,
97
+ FOREIGN KEY (feature_id) REFERENCES features(id) ON DELETE CASCADE,
98
+ UNIQUE(feature_id, kind, target, name)
99
+ );
100
+ INSERT INTO feature_dependencies_new SELECT * FROM feature_dependencies;
101
+ DROP TABLE feature_dependencies;
102
+ ALTER TABLE feature_dependencies_new RENAME TO feature_dependencies;
103
+
104
+ -- feature_files: recreate to pick up new parent table
105
+ CREATE TABLE feature_files_new (
106
+ id INTEGER PRIMARY KEY,
107
+ feature_id INTEGER NOT NULL,
108
+ file_path TEXT NOT NULL,
109
+ role TEXT,
110
+ FOREIGN KEY (feature_id) REFERENCES features(id) ON DELETE CASCADE,
111
+ UNIQUE(feature_id, file_path)
112
+ );
113
+ INSERT INTO feature_files_new SELECT * FROM feature_files;
114
+ DROP TABLE feature_files;
115
+ ALTER TABLE feature_files_new RENAME TO feature_files;
116
+
117
+ -- ─── 2. feature_smoke_tests: deduplicate + add UNIQUE constraint ──
118
+
119
+ -- Remove duplicates, keeping the row with the lowest id per (feature_id, command)
120
+ DELETE FROM feature_smoke_tests WHERE id NOT IN (
121
+ SELECT MIN(id) FROM feature_smoke_tests GROUP BY feature_id, command
122
+ );
123
+
124
+ CREATE TABLE feature_smoke_tests_new (
125
+ id INTEGER PRIMARY KEY,
126
+ feature_id INTEGER NOT NULL,
127
+ command TEXT NOT NULL,
128
+ description TEXT,
129
+ FOREIGN KEY (feature_id) REFERENCES features(id) ON DELETE CASCADE,
130
+ UNIQUE(feature_id, command)
131
+ );
132
+ INSERT INTO feature_smoke_tests_new SELECT * FROM feature_smoke_tests;
133
+ DROP TABLE feature_smoke_tests;
134
+ ALTER TABLE feature_smoke_tests_new RENAME TO feature_smoke_tests;
135
+
136
+ -- ─── 3. command_rules: add CHECK on strategy ─────────────────
137
+
138
+ CREATE TABLE command_rules_new (
139
+ id INTEGER PRIMARY KEY,
140
+ project TEXT,
141
+ pattern TEXT NOT NULL,
142
+ strategy TEXT NOT NULL DEFAULT 'summary' CHECK (strategy IN ('summary', 'truncate')),
143
+ max_lines INTEGER,
144
+ reason TEXT,
145
+ times_seen INTEGER DEFAULT 0,
146
+ avg_output_bytes INTEGER DEFAULT 0,
147
+ enabled INTEGER DEFAULT 1,
148
+ created_at TIMESTAMP DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
149
+ updated_at TIMESTAMP DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
150
+ UNIQUE(project, pattern)
151
+ );
152
+ INSERT INTO command_rules_new SELECT * FROM command_rules;
153
+ DROP TABLE command_rules;
154
+ ALTER TABLE command_rules_new RENAME TO command_rules;
155
+
156
+ CREATE INDEX IF NOT EXISTS idx_command_rules_pattern ON command_rules(pattern);
package/db/migrate.sh CHANGED
@@ -9,6 +9,9 @@ EAGLE_MEM_DIR="${EAGLE_MEM_DIR:-$HOME/.eagle-mem}"
9
9
  DB="$EAGLE_MEM_DIR/memory.db"
10
10
  SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
11
11
 
12
+ # Restrict permissions: DB and config contain session data and may contain
13
+ # residual secrets despite redaction. Owner-only access (700 dir, 600 files).
14
+ umask 077
12
15
  mkdir -p "$EAGLE_MEM_DIR"
13
16
 
14
17
  run_migration() {
@@ -31,6 +31,10 @@ esac
31
31
 
32
32
  project=$(eagle_project_from_cwd "$cwd")
33
33
 
34
+ # Ensure session row exists before inserting observations (FK constraint).
35
+ # PostToolUse can race SessionStart — the session row might not exist yet.
36
+ eagle_upsert_session "$session_id" "$project" "$cwd" "" ""
37
+
34
38
  files_read="[]"
35
39
  files_modified="[]"
36
40
  tool_summary=""
@@ -237,6 +241,8 @@ case "$tool_name" in
237
241
  ;;
238
242
  esac
239
243
 
240
- eagle_insert_observation "$session_id" "$project" "$tool_name" "$tool_summary" "$files_read" "$files_modified" "$output_bytes" "$output_lines" "$command_category"
244
+ if ! eagle_insert_observation "$session_id" "$project" "$tool_name" "$tool_summary" "$files_read" "$files_modified" "$output_bytes" "$output_lines" "$command_category"; then
245
+ eagle_log "ERROR" "PostToolUse: observation insert failed for session=$session_id tool=$tool_name"
246
+ fi
241
247
 
242
248
  exit 0
@@ -49,6 +49,7 @@ case "$cmd" in
49
49
  [ -z "$changed_file" ] && continue
50
50
  fname=$(basename "$changed_file")
51
51
  fname_esc=$(eagle_sql_escape "$fname")
52
+ fname_like=$(eagle_like_escape "$fname_esc")
52
53
 
53
54
  feature_hits=$(eagle_db "SELECT DISTINCT f.name,
54
55
  (SELECT GROUP_CONCAT(fst.command, '; ')
@@ -60,7 +61,7 @@ case "$cmd" in
60
61
  JOIN feature_files ff ON ff.feature_id = f.id
61
62
  WHERE f.project = '$p_esc'
62
63
  AND f.status = 'active'
63
- AND (ff.file_path LIKE '%$fname_esc' OR ff.file_path LIKE '%$fname_esc%');")
64
+ AND (ff.file_path LIKE '%$fname_like' ESCAPE '\\' OR ff.file_path LIKE '%$fname_like%' ESCAPE '\\');")
64
65
 
65
66
  while IFS='|' read -r feat_name feat_smoke feat_deps feat_verified; do
66
67
  [ -z "$feat_name" ] && continue
@@ -38,4 +38,7 @@ fi
38
38
  eagle_end_session "$session_id"
39
39
  eagle_log "INFO" "SessionEnd: session=$session_id marked completed"
40
40
 
41
+ # Prune observations older than 90 days (keeps DB size bounded)
42
+ eagle_prune_observations 90 "$project"
43
+
41
44
  exit 0
package/hooks/stop.sh CHANGED
@@ -154,8 +154,11 @@ key_files=$(echo "$key_files" | eagle_redact)
154
154
  # ─── Write to database ─────────────────────────────────────
155
155
 
156
156
  if [ -n "$request" ] || [ -n "$completed" ] || [ -n "$learned" ]; then
157
- eagle_insert_summary "$session_id" "$project" "$request" "$investigated" "$learned" "$completed" "$next_steps" "$files_read" "$files_modified" "$notes" "$decisions" "$gotchas" "$key_files"
158
- eagle_log "INFO" "Stop: summary saved for session=$session_id"
157
+ if eagle_insert_summary "$session_id" "$project" "$request" "$investigated" "$learned" "$completed" "$next_steps" "$files_read" "$files_modified" "$notes" "$decisions" "$gotchas" "$key_files"; then
158
+ eagle_log "INFO" "Stop: summary saved for session=$session_id"
159
+ else
160
+ eagle_log "ERROR" "Stop: summary insert FAILED for session=$session_id — check DB constraints"
161
+ fi
159
162
  fi
160
163
 
161
164
  exit 0
package/lib/common.sh CHANGED
@@ -13,6 +13,10 @@ EAGLE_SKILLS_DIR="$HOME/.claude/skills"
13
13
  eagle_log() {
14
14
  local level="$1"
15
15
  shift
16
+ # Ensure log file is owner-only (may contain debug data)
17
+ if [ ! -f "$EAGLE_MEM_LOG" ]; then
18
+ touch "$EAGLE_MEM_LOG" 2>/dev/null && chmod 600 "$EAGLE_MEM_LOG" 2>/dev/null
19
+ fi
16
20
  echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] [$level] $*" >> "$EAGLE_MEM_LOG" 2>/dev/null || true
17
21
  }
18
22
 
@@ -42,6 +46,12 @@ eagle_fts_sanitize() {
42
46
  printf '%s' "$1" | sed 's/[*"(){}^~:]/ /g' | sed 's/ */ /g; s/^ //; s/ $//'
43
47
  }
44
48
 
49
+ # Escape SQL LIKE wildcards (% and _) so literal filenames match exactly.
50
+ # Apply AFTER eagle_sql_escape, since this only handles LIKE metacharacters.
51
+ eagle_like_escape() {
52
+ printf '%s' "$1" | sed 's/%/\\%/g; s/_/\\_/g'
53
+ }
54
+
45
55
  # Validate a session ID is safe for use in file paths (no traversal).
46
56
  # Claude Code session IDs are UUIDs or hex strings — reject anything else.
47
57
  eagle_validate_session_id() {
@@ -64,21 +74,34 @@ eagle_read_stdin() {
64
74
  # Stripe/AWS/GitHub/Anthropic/OpenAI key patterns, named env vars.
65
75
  eagle_redact() {
66
76
  sed -E \
67
- -e 's/(Bearer )[^ ]*/\1[REDACTED]/gi' \
68
- -e 's/(api[_-]?key[= :])[^ ]*/\1[REDACTED]/gi' \
69
- -e 's/(password[= :])[^ ]*/\1[REDACTED]/gi' \
70
- -e 's/(secret[= :])[^ ]*/\1[REDACTED]/gi' \
71
- -e 's/(token[= :])[^ ]*/\1[REDACTED]/gi' \
72
- -e 's/(Authorization: )[^ ]*/\1[REDACTED]/gi' \
77
+ -e 's/(Bearer )[^[:space:],;"'"'"']*/\1[REDACTED]/gi' \
78
+ -e 's/(api[_-]?key[= :])[^[:space:],;"'"'"']*/\1[REDACTED]/gi' \
79
+ -e 's/(password[= :])[^[:space:],;"'"'"']*/\1[REDACTED]/gi' \
80
+ -e 's/(secret[= :])[^[:space:],;"'"'"']*/\1[REDACTED]/gi' \
81
+ -e 's/(token[= :])[^[:space:],;"'"'"']*/\1[REDACTED]/gi' \
82
+ -e 's/(Authorization: )[^[:space:],;"'"'"']*/\1[REDACTED]/gi' \
83
+ -e 's/(client_secret[= :])[^[:space:],;"'"'"']*/\1[REDACTED]/gi' \
84
+ -e 's/(private_key[= :])[^[:space:],;"'"'"']*/\1[REDACTED]/gi' \
85
+ -e 's/(access_token[= :])[^[:space:],;"'"'"']*/\1[REDACTED]/gi' \
73
86
  -e 's/sk_live_[A-Za-z0-9]+/[REDACTED]/g' \
74
87
  -e 's/sk_test_[A-Za-z0-9]+/[REDACTED]/g' \
88
+ -e 's/whsec_[A-Za-z0-9]+/[REDACTED]/g' \
75
89
  -e 's/AKIA[A-Z0-9]{16}/[REDACTED]/g' \
76
90
  -e 's/ghp_[A-Za-z0-9]{36}/[REDACTED]/g' \
77
91
  -e 's/gho_[A-Za-z0-9]{36}/[REDACTED]/g' \
92
+ -e 's/glpat-[A-Za-z0-9_-]{20,}/[REDACTED]/g' \
78
93
  -e 's/sk-ant-[A-Za-z0-9_-]+/[REDACTED]/g' \
79
94
  -e 's/sk-[A-Za-z0-9]{20,}/[REDACTED]/g' \
80
- -e 's/(ANTHROPIC_API_KEY[= :])[^ ]*/\1[REDACTED]/g' \
81
- -e 's/(OPENAI_API_KEY[= :])[^ ]*/\1[REDACTED]/g'
95
+ -e 's/AIza[0-9A-Za-z_-]{35}/[REDACTED]/g' \
96
+ -e 's/xox[abps]-[A-Za-z0-9-]+/[REDACTED]/g' \
97
+ -e 's/eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/[REDACTED_JWT]/g' \
98
+ -e 's|(https?://[^/:]+:)[^@]+(@)|\1[REDACTED]\2|g' \
99
+ -e 's/-----BEGIN [A-Z ]*PRIVATE KEY-----/[REDACTED_PRIVATE_KEY]/g' \
100
+ -e 's/(ANTHROPIC_API_KEY[= :])[^[:space:],;"'"'"']*/\1[REDACTED]/g' \
101
+ -e 's/(OPENAI_API_KEY[= :])[^[:space:],;"'"'"']*/\1[REDACTED]/g' \
102
+ -e 's/(GOOGLE_API_KEY[= :])[^[:space:],;"'"'"']*/\1[REDACTED]/g' \
103
+ -e 's/(SLACK_TOKEN[= :])[^[:space:],;"'"'"']*/\1[REDACTED]/g' \
104
+ -e 's/(DATABASE_URL[= :])[^[:space:],;"'"'"']*/\1[REDACTED]/g'
82
105
  }
83
106
 
84
107
  # Collect project files into a destination file.
package/lib/db.sh CHANGED
@@ -14,15 +14,45 @@ PRAGMA trusted_schema=ON;
14
14
  .output stdout"
15
15
 
16
16
  eagle_db() {
17
- { echo "$EAGLE_DB_SETUP"; echo "$*"; } | sqlite3 "$EAGLE_MEM_DB" 2>>"$EAGLE_MEM_LOG"
17
+ local _eagle_db_err
18
+ _eagle_db_err=$(mktemp 2>/dev/null || echo "/tmp/_eagle_db_err.$$")
19
+ local _eagle_db_out
20
+ _eagle_db_out=$({ echo "$EAGLE_DB_SETUP"; echo "$*"; } | sqlite3 "$EAGLE_MEM_DB" 2>"$_eagle_db_err")
21
+ local _eagle_db_rc=$?
22
+ if [ -s "$_eagle_db_err" ]; then
23
+ cat "$_eagle_db_err" >> "$EAGLE_MEM_LOG" 2>/dev/null
24
+ fi
25
+ rm -f "$_eagle_db_err" 2>/dev/null
26
+ [ -n "$_eagle_db_out" ] && printf '%s\n' "$_eagle_db_out"
27
+ return $_eagle_db_rc
18
28
  }
19
29
 
20
30
  eagle_db_pipe() {
21
- { echo "$EAGLE_DB_SETUP"; echo ".bail on"; cat; } | sqlite3 "$EAGLE_MEM_DB" 2>>"$EAGLE_MEM_LOG"
31
+ local _eagle_db_err
32
+ _eagle_db_err=$(mktemp 2>/dev/null || echo "/tmp/_eagle_db_pipe_err.$$")
33
+ local _eagle_db_out
34
+ _eagle_db_out=$({ echo "$EAGLE_DB_SETUP"; echo ".bail on"; cat; } | sqlite3 "$EAGLE_MEM_DB" 2>"$_eagle_db_err")
35
+ local _eagle_db_rc=$?
36
+ if [ -s "$_eagle_db_err" ]; then
37
+ cat "$_eagle_db_err" >> "$EAGLE_MEM_LOG" 2>/dev/null
38
+ fi
39
+ rm -f "$_eagle_db_err" 2>/dev/null
40
+ [ -n "$_eagle_db_out" ] && printf '%s\n' "$_eagle_db_out"
41
+ return $_eagle_db_rc
22
42
  }
23
43
 
24
44
  eagle_db_json() {
25
- { echo "$EAGLE_DB_SETUP"; echo ".mode json"; echo "$*"; } | sqlite3 "$EAGLE_MEM_DB" 2>>"$EAGLE_MEM_LOG"
45
+ local _eagle_db_err
46
+ _eagle_db_err=$(mktemp 2>/dev/null || echo "/tmp/_eagle_db_json_err.$$")
47
+ local _eagle_db_out
48
+ _eagle_db_out=$({ echo "$EAGLE_DB_SETUP"; echo ".mode json"; echo "$*"; } | sqlite3 "$EAGLE_MEM_DB" 2>"$_eagle_db_err")
49
+ local _eagle_db_rc=$?
50
+ if [ -s "$_eagle_db_err" ]; then
51
+ cat "$_eagle_db_err" >> "$EAGLE_MEM_LOG" 2>/dev/null
52
+ fi
53
+ rm -f "$_eagle_db_err" 2>/dev/null
54
+ [ -n "$_eagle_db_out" ] && printf '%s\n' "$_eagle_db_out"
55
+ return $_eagle_db_rc
26
56
  }
27
57
 
28
58
  eagle_ensure_db() {
@@ -615,7 +645,7 @@ eagle_add_feature_smoke_test() {
615
645
  local command; command=$(eagle_sql_escape "$2")
616
646
  local description; description=$(eagle_sql_escape "${3:-}")
617
647
 
618
- eagle_db "INSERT INTO feature_smoke_tests (feature_id, command, description)
648
+ eagle_db "INSERT OR IGNORE INTO feature_smoke_tests (feature_id, command, description)
619
649
  VALUES ($feature_id, '$command', '$description');"
620
650
  }
621
651
 
@@ -690,6 +720,7 @@ eagle_find_features_for_file() {
690
720
  local file_path="$2"
691
721
  local fname; fname=$(basename "$file_path")
692
722
  local fname_esc; fname_esc=$(eagle_sql_escape "$fname")
723
+ local fname_like; fname_like=$(eagle_like_escape "$fname_esc")
693
724
 
694
725
  eagle_db "SELECT f.name, f.description, f.last_verified_at,
695
726
  ff.role,
@@ -703,7 +734,7 @@ eagle_find_features_for_file() {
703
734
  JOIN feature_files ff ON ff.feature_id = f.id
704
735
  WHERE f.project = '$project'
705
736
  AND f.status = 'active'
706
- AND (ff.file_path LIKE '%$fname_esc' OR ff.file_path LIKE '%$fname_esc%')
737
+ AND (ff.file_path LIKE '%$fname_like' ESCAPE '\\' OR ff.file_path LIKE '%$fname_like%' ESCAPE '\\')
707
738
  ORDER BY f.updated_at DESC
708
739
  LIMIT 3;"
709
740
  }
package/lib/provider.sh CHANGED
@@ -50,19 +50,30 @@ eagle_config_set() {
50
50
  local key="$2"
51
51
  local value="$3"
52
52
 
53
+ # Validate section/key are alphanumeric+underscore (safe for grep/sed patterns)
54
+ if [[ ! "$section" =~ ^[A-Za-z0-9_-]+$ ]] || [[ ! "$key" =~ ^[A-Za-z0-9_-]+$ ]]; then
55
+ eagle_log "ERROR" "config_set: invalid section/key: [$section] $key"
56
+ return 1
57
+ fi
58
+
53
59
  if [ ! -f "$EAGLE_CONFIG_FILE" ]; then
54
60
  eagle_config_init
55
61
  fi
56
62
 
63
+ # Escape sed metacharacters in value to prevent injection via |, &, \, /
64
+ local safe_value
65
+ safe_value=$(printf '%s' "$value" | sed 's/[|&/\]/\\&/g')
66
+
57
67
  if grep -q "^\[${section}\]" "$EAGLE_CONFIG_FILE" 2>/dev/null; then
58
68
  if grep -q "^[[:space:]]*${key}[[:space:]]*=" "$EAGLE_CONFIG_FILE" 2>/dev/null; then
59
- sed -i '' "s|^[[:space:]]*${key}[[:space:]]*=.*|${key} = \"${value}\"|" "$EAGLE_CONFIG_FILE"
69
+ sed -i '' "s|^[[:space:]]*${key}[[:space:]]*=.*|${key} = \"${safe_value}\"|" "$EAGLE_CONFIG_FILE"
60
70
  else
61
71
  sed -i '' "/^\[${section}\]/a\\
62
- ${key} = \"${value}\"
72
+ ${key} = \"${safe_value}\"
63
73
  " "$EAGLE_CONFIG_FILE"
64
74
  fi
65
75
  else
76
+ # printf is safe — no sed interpolation needed for append
66
77
  printf '\n[%s]\n%s = "%s"\n' "$section" "$key" "$value" >> "$EAGLE_CONFIG_FILE"
67
78
  fi
68
79
  }
@@ -115,7 +126,10 @@ eagle_config_init() {
115
126
  model="gpt-4o-mini"
116
127
  fi
117
128
 
118
- cat > "$EAGLE_CONFIG_FILE" << TOML
129
+ # Create config with restrictive permissions from the start (no TOCTOU window)
130
+ (
131
+ umask 077
132
+ cat > "$EAGLE_CONFIG_FILE" << TOML
119
133
  # Eagle Mem configuration
120
134
  # Docs: https://github.com/eagleisbatman/eagle-mem
121
135
 
@@ -144,7 +158,7 @@ schedule = "manual"
144
158
  # Additional secret patterns (regex) beyond built-in defaults
145
159
  # extra_patterns = ["MY_CUSTOM_SECRET_.*"]
146
160
  TOML
147
-
161
+ )
148
162
  eagle_log "INFO" "Config initialized: provider=$provider model=$model"
149
163
  }
150
164
 
@@ -236,11 +250,12 @@ _eagle_call_anthropic() {
236
250
  messages: [{role: "user", content: $prompt}]
237
251
  }')
238
252
 
253
+ # Pass API key via config stdin to avoid exposing it in process list (ps aux)
239
254
  local response
240
255
  response=$(curl -sf "https://api.anthropic.com/v1/messages" \
241
256
  --connect-timeout 5 \
242
257
  --max-time 120 \
243
- -H "x-api-key: ${api_key}" \
258
+ -K <(printf 'header = "x-api-key: %s"' "$api_key") \
244
259
  -H "anthropic-version: 2023-06-01" \
245
260
  -H "content-type: application/json" \
246
261
  -d "$body" 2>/dev/null)
@@ -280,11 +295,12 @@ _eagle_call_openai() {
280
295
  ]
281
296
  }')
282
297
 
298
+ # Pass API key via config stdin to avoid exposing it in process list (ps aux)
283
299
  local response
284
300
  response=$(curl -sf "https://api.openai.com/v1/chat/completions" \
285
301
  --connect-timeout 5 \
286
302
  --max-time 120 \
287
- -H "Authorization: Bearer ${api_key}" \
303
+ -K <(printf 'header = "Authorization: Bearer %s"' "$api_key") \
288
304
  -H "content-type: application/json" \
289
305
  -d "$body" 2>/dev/null)
290
306
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eagle-mem",
3
- "version": "3.0.0",
3
+ "version": "3.0.1",
4
4
  "description": "Persistent memory for Claude Code — SQLite + FTS5, no daemon, no bloat",
5
5
  "bin": {
6
6
  "eagle-mem": "bin/eagle-mem"
package/scripts/curate.sh CHANGED
@@ -213,6 +213,16 @@ If no rules needed, output: NONE"
213
213
  max_lines=$(echo "$max_lines" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
214
214
  reason=$(echo "$reason" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
215
215
 
216
+ # Guard: skip malformed lines missing required fields
217
+ if [ -z "$pattern" ] || [ -z "$strategy" ]; then
218
+ eagle_log "WARN" "Curator: skipping malformed RULE line: $line"
219
+ continue
220
+ fi
221
+ case "$strategy" in summary|truncate) ;; *)
222
+ eagle_log "WARN" "Curator: skipping RULE with invalid strategy '$strategy'"
223
+ continue
224
+ ;; esac
225
+
216
226
  [ "$max_lines" = "-" ] && max_lines=""
217
227
 
218
228
  if [ "$DRY_RUN" -eq 1 ]; then
@@ -292,6 +302,12 @@ Rules:
292
302
  fdesc=$(echo "$fdesc" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
293
303
  ffiles=$(echo "$ffiles" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
294
304
 
305
+ # Guard: skip malformed lines missing required name
306
+ if [ -z "$fname" ]; then
307
+ eagle_log "WARN" "Curator: skipping malformed FEATURE line: $line"
308
+ continue
309
+ fi
310
+
295
311
  if [ "$DRY_RUN" -eq 1 ]; then
296
312
  eagle_info " Feature: $fname — $fdesc"
297
313
  eagle_info " Files: $ffiles"
@@ -152,7 +152,10 @@ eagle_ok "Files copied to $EAGLE_MEM_DIR"
152
152
 
153
153
  # ─── Run migrations ────────────────────────────────────────
154
154
 
155
- "$EAGLE_MEM_DIR/db/migrate.sh" 2>/dev/null | grep -v -E '^(wal|5000|Eagle Mem database)$' > /dev/null
155
+ if ! "$EAGLE_MEM_DIR/db/migrate.sh" 2>/dev/null; then
156
+ eagle_err "Database migration failed"
157
+ exit 1
158
+ fi
156
159
  eagle_ok "Database ready"
157
160
 
158
161
  # ─── Patch settings.json ───────────────────────────────────
@@ -242,7 +245,7 @@ else
242
245
  echo ""
243
246
  eagle_ok "Statusline ${DIM}(manual patch needed — instructions above)${RESET}"
244
247
  else
245
- eagle_ok "Statusline ${DIM}(already has Eagle Mem)${RESET}"
248
+ eagle_ok "Statusline ${DIM}(existing cannot auto-patch; add Eagle Mem manually)${RESET}"
246
249
  fi
247
250
  fi
248
251
 
@@ -18,7 +18,7 @@ eagle_header "Uninstall"
18
18
  # ─── Remove hooks from settings.json ──────────────────────
19
19
 
20
20
  if [ -f "$SETTINGS" ] && command -v jq &>/dev/null; then
21
- for event in SessionStart Stop PostToolUse SessionEnd UserPromptSubmit; do
21
+ for event in SessionStart Stop PostToolUse PreToolUse SessionEnd UserPromptSubmit; do
22
22
  if jq -e ".hooks.${event}" "$SETTINGS" &>/dev/null; then
23
23
  tmp=$(mktemp)
24
24
  jq ".hooks.${event} = [.hooks.${event}[]? | select(any(.hooks[]?; .command | contains(\"eagle-mem\")) | not)]" "$SETTINGS" > "$tmp" && mv "$tmp" "$SETTINGS"
package/scripts/update.sh CHANGED
@@ -46,7 +46,11 @@ eagle_ok "Files updated"
46
46
 
47
47
  # ─── Run pending migrations ────────────────────────────────
48
48
 
49
- migration_output=$("$EAGLE_MEM_DIR/db/migrate.sh" 2>/dev/null | grep -v -E '^(wal|5000)$')
49
+ migration_output=$("$EAGLE_MEM_DIR/db/migrate.sh" 2>&1) || {
50
+ eagle_err "Database migration failed"
51
+ eagle_err "$migration_output"
52
+ exit 1
53
+ }
50
54
  if echo "$migration_output" | grep -q "applied:"; then
51
55
  echo "$migration_output" | grep "applied:" | while read -r line; do
52
56
  eagle_ok "Migration: ${line#*applied: }"