Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions docs/public/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Settings are managed in `~/.claude-mem/settings.json`. The file is auto-created
| `CLAUDE_MEM_CONTEXT_OBSERVATIONS` | `50` | Number of observations to inject |
| `CLAUDE_MEM_WORKER_PORT` | `37777` | Worker service port |
| `CLAUDE_MEM_SKIP_TOOLS` | `ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion` | Comma-separated tools to exclude from observations |
| `CLAUDE_MEM_EXCLUDE_PROJECTS` | _(empty)_ | Comma-separated glob patterns for projects to exclude from memory |

### System Configuration

Expand Down Expand Up @@ -384,6 +385,38 @@ Control which tools are excluded from observations. Edit `~/.claude-mem/settings

Changes take effect on the next tool execution (no worker restart needed).

### Exclude Projects from Memory

Prevent claude-mem from recording observations for specific projects. Supports glob patterns for flexible matching.

Edit `~/.claude-mem/settings.json`:
```json
{
"CLAUDE_MEM_EXCLUDE_PROJECTS": "test-project, tmp-*, claude-analysis-*"
}
```

**Pattern Support:**
- `project-name` - Exact match
- `tmp-*` - Matches tmp-foo, tmp-bar, tmp-anything
- `*-test` - Matches foo-test, bar-test
- `test-?` - Matches test-1, test-a (single character only)

**Use Cases:**
- Temporary test projects
- Private or sensitive work
- Throwaway experiments
- CI/build directories
- Automated analysis projects (e.g., `claude-analysis-*`)

**What Gets Excluded:**
- No context injection at session start
- No session tracking on UserPromptSubmit
- No observations recorded from tool usage
- Zero overhead - worker is never contacted for excluded projects

Changes take effect immediately on the next session start (no worker restart needed).

## Advanced Configuration

### Hook Timeouts
Expand Down
14 changes: 7 additions & 7 deletions plugin/scripts/cleanup-hook.js

Large diffs are not rendered by default.

110 changes: 55 additions & 55 deletions plugin/scripts/context-generator.cjs

Large diffs are not rendered by default.

14 changes: 7 additions & 7 deletions plugin/scripts/context-hook.js

Large diffs are not rendered by default.

47 changes: 35 additions & 12 deletions plugin/scripts/mcp-server.cjs

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions plugin/scripts/new-hook.js

Large diffs are not rendered by default.

14 changes: 7 additions & 7 deletions plugin/scripts/save-hook.js

Large diffs are not rendered by default.

18 changes: 9 additions & 9 deletions plugin/scripts/summary-hook.js

Large diffs are not rendered by default.

20 changes: 10 additions & 10 deletions plugin/scripts/user-message-hook.js

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions plugin/scripts/worker-cli.js

Large diffs are not rendered by default.

374 changes: 199 additions & 175 deletions plugin/scripts/worker-service.cjs

Large diffs are not rendered by default.

Binary file modified plugin/skills/mem-search.zip
Binary file not shown.
8 changes: 8 additions & 0 deletions src/hooks/cleanup-hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import { stdin } from 'process';
import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.js';
import { HOOK_TIMEOUTS } from '../shared/hook-constants.js';
import { getProjectName, isProjectExcluded } from '../utils/project-name.js';

export interface SessionEndInput {
session_id: string;
Expand All @@ -19,6 +20,13 @@ export interface SessionEndInput {
* Cleanup Hook Main Logic - Fire-and-forget HTTP client
*/
async function cleanupHook(input?: SessionEndInput): Promise<void> {
// Check project exclusion early - before worker contact
const project = getProjectName(process.cwd());
if (isProjectExcluded(project)) {
console.log('{"continue": true, "suppressOutput": true}');
process.exit(0);
}

// Ensure worker is running before any other logic
await ensureWorkerRunning();

Expand Down
12 changes: 9 additions & 3 deletions src/hooks/context-hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import { stdin } from "process";
import { ensureWorkerRunning, getWorkerPort } from "../shared/worker-utils.js";
import { HOOK_TIMEOUTS } from "../shared/hook-constants.js";
import { getProjectName } from "../utils/project-name.js";
import { getProjectName, isProjectExcluded } from "../utils/project-name.js";

export interface SessionStartInput {
session_id: string;
Expand All @@ -19,11 +19,17 @@ export interface SessionStartInput {
}

async function contextHook(input?: SessionStartInput): Promise<string> {
const cwd = input?.cwd ?? process.cwd();
const project = getProjectName(cwd);

// Early exit for excluded projects - no context injection
if (isProjectExcluded(project)) {
return '';
}

// Ensure worker is running before any other logic
await ensureWorkerRunning();

const cwd = input?.cwd ?? process.cwd();
const project = getProjectName(cwd);
const port = getWorkerPort();

const url = `http://127.0.0.1:${port}/api/context/inject?project=${encodeURIComponent(project)}`;
Expand Down
14 changes: 10 additions & 4 deletions src/hooks/new-hook.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { stdin } from 'process';
import { STANDARD_HOOK_RESPONSE } from './hook-response.js';
import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.js';
import { getProjectName } from '../utils/project-name.js';
import { getProjectName, isProjectExcluded } from '../utils/project-name.js';

export interface UserPromptSubmitInput {
session_id: string;
Expand All @@ -14,16 +14,22 @@ export interface UserPromptSubmitInput {
* New Hook Main Logic
*/
async function newHook(input?: UserPromptSubmitInput): Promise<void> {
// Ensure worker is running before any other logic
await ensureWorkerRunning();

if (!input) {
throw new Error('newHook requires input');
}

const { session_id, cwd, prompt } = input;
const project = getProjectName(cwd);

// Early exit for excluded projects - no session tracking
if (isProjectExcluded(project)) {
console.log(STANDARD_HOOK_RESPONSE);
return;
}

// Ensure worker is running before any other logic
await ensureWorkerRunning();

const port = getWorkerPort();

// Initialize session via HTTP - handles DB operations and privacy checks
Expand Down
14 changes: 11 additions & 3 deletions src/hooks/save-hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { STANDARD_HOOK_RESPONSE } from './hook-response.js';
import { logger } from '../utils/logger.js';
import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.js';
import { HOOK_TIMEOUTS } from '../shared/hook-constants.js';
import { getProjectName, isProjectExcluded } from '../utils/project-name.js';

export interface PostToolUseInput {
session_id: string;
Expand All @@ -24,15 +25,22 @@ export interface PostToolUseInput {
* Save Hook Main Logic - Fire-and-forget HTTP client
*/
async function saveHook(input?: PostToolUseInput): Promise<void> {
// Ensure worker is running before any other logic
await ensureWorkerRunning();

if (!input) {
throw new Error('saveHook requires input');
}

const { session_id, cwd, tool_name, tool_input, tool_response } = input;

// Early exit for excluded projects - no observation recording
const project = getProjectName(cwd);
if (isProjectExcluded(project)) {
console.log(STANDARD_HOOK_RESPONSE);
return;
}

// Ensure worker is running before any other logic
await ensureWorkerRunning();

const port = getWorkerPort();

const toolStr = logger.formatTool(tool_name, tool_input);
Expand Down
16 changes: 12 additions & 4 deletions src/hooks/summary-hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { logger } from '../utils/logger.js';
import { ensureWorkerRunning, getWorkerPort } from '../shared/worker-utils.js';
import { HOOK_TIMEOUTS } from '../shared/hook-constants.js';
import { extractLastMessage } from '../shared/transcript-parser.js';
import { getProjectName, isProjectExcluded } from '../utils/project-name.js';

export interface StopInput {
session_id: string;
Expand All @@ -26,14 +27,21 @@ export interface StopInput {
* Summary Hook Main Logic - Fire-and-forget HTTP client
*/
async function summaryHook(input?: StopInput): Promise<void> {
// Ensure worker is running before any other logic
await ensureWorkerRunning();

if (!input) {
throw new Error('summaryHook requires input');
}

const { session_id } = input;
const { session_id, cwd } = input;
const project = getProjectName(cwd);

// Early exit for excluded projects - no summary generation needed
if (isProjectExcluded(project)) {
console.log(STANDARD_HOOK_RESPONSE);
return;
}

// Ensure worker is running before any other logic
await ensureWorkerRunning();

const port = getWorkerPort();

Expand Down
10 changes: 8 additions & 2 deletions src/hooks/user-message-hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,21 @@
* has been loaded into their session. Uses stderr as the communication channel
* since it's currently the only way to display messages in Claude Code UI.
*/
import { basename } from "path";
import { ensureWorkerRunning, getWorkerPort } from "../shared/worker-utils.js";
import { HOOK_EXIT_CODES } from "../shared/hook-constants.js";
import { getProjectName, isProjectExcluded } from "../utils/project-name.js";

const project = getProjectName(process.cwd());

// Early exit for excluded projects - no user message display needed
if (isProjectExcluded(project)) {
process.exit(HOOK_EXIT_CODES.USER_MESSAGE_ONLY);
}

// Ensure worker is running
await ensureWorkerRunning();

const port = getWorkerPort();
const project = basename(process.cwd());

// Fetch formatted context directly from worker API
const response = await fetch(
Expand Down
2 changes: 2 additions & 0 deletions src/services/worker/http/routes/SettingsRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ export class SettingsRoutes extends BaseRouteHandler {
'CLAUDE_MEM_CONTEXT_OBSERVATIONS',
'CLAUDE_MEM_WORKER_PORT',
'CLAUDE_MEM_WORKER_HOST',
'CLAUDE_MEM_SKIP_TOOLS',
'CLAUDE_MEM_EXCLUDE_PROJECTS',
// System Configuration
'CLAUDE_MEM_DATA_DIR',
'CLAUDE_MEM_LOG_LEVEL',
Expand Down
2 changes: 2 additions & 0 deletions src/shared/SettingsDefaultsManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface SettingsDefaults {
CLAUDE_MEM_WORKER_PORT: string;
CLAUDE_MEM_WORKER_HOST: string;
CLAUDE_MEM_SKIP_TOOLS: string;
CLAUDE_MEM_EXCLUDE_PROJECTS: string;
// System Configuration
CLAUDE_MEM_DATA_DIR: string;
CLAUDE_MEM_LOG_LEVEL: string;
Expand Down Expand Up @@ -50,6 +51,7 @@ export class SettingsDefaultsManager {
CLAUDE_MEM_WORKER_PORT: '37777',
CLAUDE_MEM_WORKER_HOST: '127.0.0.1',
CLAUDE_MEM_SKIP_TOOLS: 'ListMcpResourcesTool,SlashCommand,Skill,TodoWrite,AskUserQuestion',
CLAUDE_MEM_EXCLUDE_PROJECTS: '', // Comma-separated list of project names to exclude from memory
// System Configuration
CLAUDE_MEM_DATA_DIR: join(homedir(), '.claude-mem'),
CLAUDE_MEM_LOG_LEVEL: 'INFO',
Expand Down
68 changes: 68 additions & 0 deletions src/utils/project-name.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import path from 'path';
import { homedir } from 'os';
import { logger } from './logger.js';
import { SettingsDefaultsManager, type SettingsDefaults } from '../shared/SettingsDefaultsManager.js';

/**
* Extract project name from working directory path
Expand Down Expand Up @@ -37,3 +39,69 @@ export function getProjectName(cwd: string | null | undefined): string {

return basename;
}

/**
* Convert a glob pattern to a RegExp
* Supports: * (any chars), ? (single char)
*
* @param pattern - Glob pattern (e.g., "claude-analysis-*")
* @returns RegExp that matches the pattern
*/
function globToRegex(pattern: string): RegExp {
const escaped = pattern
.replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape regex special chars (except * and ?)
.replace(/\*/g, '.*') // * → match any characters
.replace(/\?/g, '.'); // ? → match single character
return new RegExp(`^${escaped}$`);
}

/**
* Check if a pattern contains glob wildcards
*/
function isGlobPattern(pattern: string): boolean {
return pattern.includes('*') || pattern.includes('?');
}

/**
* Check if a project is excluded from claude-mem
* Uses CLAUDE_MEM_EXCLUDE_PROJECTS setting (comma-separated list)
* Supports exact matches and glob patterns (*, ?)
*
* Examples:
* - "claude-mem" → exact match
* - "claude-analysis-*" → matches claude-analysis-abc123, claude-analysis-xyz
* - "test-?" → matches test-1, test-a, but not test-12
*
* @param project - Project name to check
* @param settings - Optional pre-loaded settings (loads from file if not provided)
* @returns true if project should be excluded
*/
export function isProjectExcluded(project: string, settings?: SettingsDefaults): boolean {
const settingsToUse = settings ?? SettingsDefaultsManager.loadFromFile(
path.join(homedir(), '.claude-mem', 'settings.json')
);

const excludeList = settingsToUse.CLAUDE_MEM_EXCLUDE_PROJECTS;
if (!excludeList || excludeList.trim() === '') {
return false;
}

const patterns = excludeList.split(',').map(p => p.trim()).filter(Boolean);

for (const pattern of patterns) {
if (isGlobPattern(pattern)) {
// Glob pattern match
const regex = globToRegex(pattern);
if (regex.test(project)) {
return true;
}
} else {
// Exact match
if (pattern === project) {
return true;
}
}
}

return false;
}