Root cause
Other / insufficient public mechanism detail introduced
MAESTRO: fix data integrity — content-hash deduplication, project name collision, empty project guard, stuck isProcessing
How AI contributed: Direct introductionPublished Jun 5, 2026 in
thedotmack/claude-mem
A weakness has been identified in thedotmack claude-mem up to 11.0.1. The affected element is the function computeObservationContentHash of the file src/services/sqlite/observations/store.ts of the component Observation Content Hash Handler. This manipulation causes use of weak hash. The attack can only be executed locally. The attack's complexity is rated as high. The exploitability is described as difficult. Upgrading to version 12.0.0 is sufficient to fix this issue. Patch name: f32fda8b35e9fe9329f87da65c31149362a03f97. It is suggested to upgrade the affected component.
computeObservationContentHash in src/services/sqlite/observations/store.ts concatenated memorySessionId, title and narrative with no separator before hashing, so different field tuples, such as session ab with title cd versus session abc with title d, produce the same input string and therefore the same digest. The fix inserts a NUL delimiter between the three fields so each tuple maps to a distinct byte string.
Root cause
MAESTRO: fix data integrity — content-hash deduplication, project name collision, empty project guard, stuck isProcessing
Fix
The fix inserts a NUL delimiter between the session, title, and narrative fields before hashing, so different field tuples cannot collapse to the same concatenated byte string.
Case facts
@@ -3,13 +3,50 @@ * Extracted from SessionStore.ts for modular organization */ +import { createHash } from 'crypto'; import { Database } from 'bun:sqlite'; import { logger } from '../../../utils/logger.js';+import { getCurrentProjectName } from '../../../shared/paths.js'; import type { ObservationInput, StoreObservationResult } from './types.js'; +/** Deduplication window: observations with the same content hash within this window are skipped */+const DEDUP_WINDOW_MS = 30_000;++/**+ * Compute a short content hash for deduplication.+ * Uses (memory_session_id, title, narrative) as the semantic identity of an observation.+ */+export function computeObservationContentHash(+ memorySessionId: string,+ title: string | null,+ narrative: string | null+): string {+ return createHash('sha256')+ .update((memorySessionId || '') + (title || '') + (narrative || ''))+ .digest('hex')+ .slice(0, 16);+}++/**+ * Check if a duplicate observation exists within the dedup window.+ * Returns the existing observation's id and timestamp if found, null otherwise.+ */+export function findDuplicateObservation(+ db: Database,+ contentHash: string,+ timestampEpoch: number+): { id: number; created_at_epoch: number } | null {+ const windowStart = timestampEpoch - DEDUP_WINDOW_MS;+ const stmt = db.prepare(+ 'SELECT id, created_at_epoch FROM observations WHERE content_hash = ? AND created_at_epoch > ?'+ );+ return (stmt.get(contentHash, windowStart) as { id: number; created_at_epoch: number } | null);+}+ /** * Store an observation (from SDK parsing) * Assumes session already exists (created by hook)+ * Performs content-hash deduplication: skips INSERT if an identical observation exists within 30s */ export function storeObservation( db: Database,@@ -24,16 +61,27 @@ export function storeObservation( const timestampEpoch = overrideTimestampEpoch ?? Date.now(); const timestampIso = new Date(timestampEpoch).toISOString(); + // Guard against empty project string (race condition where project isn't set yet)+ const resolvedProject = project || getCurrentProjectName();++ // Content-hash deduplication+ const contentHash = computeObservationContentHash(memorySessionId, observation.title, observation.narrative);+ const existing = findDuplicateObservation(db, contentHash, timestampEpoch);+ if (existing) {+ logger.debug('DEDUP', `Skipped duplicate observation | contentHash=${contentHash} | existingId=${existing.id}`);+ return { id: existing.id, createdAtEpoch: existing.created_at_epoch };+ }+ const stmt = db.prepare(` INSERT INTO observations (memory_session_id, project, type, title, subtitle, facts, narrative, concepts,- files_read, files_modified, prompt_number, discovery_tokens, created_at, created_at_epoch)- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)+ files_read, files_modified, prompt_number, discovery_tokens, content_hash, created_at, created_at_epoch)+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `); const result = stmt.run( memorySessionId,- project,+ resolvedProject, observation.type, observation.title, observation.subtitle,@@ -44,6 +92,7 @@ export function storeObservation( JSON.stringify(observation.files_modified), promptNumber || null, discoveryTokens,+ contentHash, timestampIso, timestampEpoch );@@ -22,7 +22,7 @@ export function computeObservationContentHash( narrative: string | null ): string { return createHash('sha256')- .update((memorySessionId || '') + (title || '') + (narrative || ''))+ .update([memorySessionId || '', title || '', narrative || ''].join('\x00')) .digest('hex') .slice(0, 16); }AI-assisted change caf7c3eb0553929cb0b8de00489a90c0541528f90353810c26ee72f88389a29f · Fix 6aa03600f1c519bcf050c1b8a399836b9f5e1503e6b02604da2a6211cece5d65