diff --git a/README.md b/README.md
index 20e8f83..7ab7b0d 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,11 @@ and the lessons learned across every project — automatically.
`UserPromptSubmit` hook.
- 💾 **Automatic capture** — conversations are stored incrementally (every N turns) and
at session end via the `Stop` hook.
+- 🏷️ **Project + user scoping** — memories are tagged per-project and per-user so
+ context never leaks across repos.
+- 📦 **Custom container tags** — define custom memory containers (e.g., `work`, `personal`,
+ `code_style`). The AI automatically picks the right container based on your instructions
+ when saving, searching, or forgetting memories.
- 🏷️ **Project + user scoping** — automatic session memories are stored per-user,
while explicit project knowledge is tagged per-repo so context never leaks across repos.
- 🔒 **Privacy-aware** — anything wrapped in `...` is redacted
@@ -94,6 +99,9 @@ Drop this file in to override defaults:
| `signalExtraction` | `boolean` | `false` | Enable signal-based filtering (only capture turns with keywords like "prefer", "decided"). |
| `signalKeywords` | `string[]` | (defaults) | Keywords that trigger signal extraction. |
| `signalTurnsBefore` | `number` | `3` | Include N turns before a signal for context. |
+| `enableCustomContainers` | `boolean` | `false` | Enable AI-driven routing to custom containers. |
+| `customContainers` | `array` | `[]` | Custom containers with `tag` and `description` (see below). |
+| `customContainerInstructions` | `string` | `""` | Free-text instructions for the AI on how to route memories to containers.
User tags are auto-derived from your `git config user.email`. Project tags are
derived from the Git common directory when available, so linked worktrees and
@@ -116,17 +124,63 @@ npx codex-supermemory status # show current install status
## Skills (fallback commands)
-These Codex skills are available as explicit commands when you need more control:
+These Codex skills are available as explicit commands when you need more control.
+All memory skills support `--container ` to target a specific custom container.
-| Skill | Usage | Description |
-| ---------------------- | ------------------------------------------ | ---------------------------------------- |
-| `/supermemory-search` | `/supermemory-search ` | Search memories manually. |
-| `/supermemory-save` | `/supermemory-save ` | Save a specific memory explicitly. |
-| `/supermemory-forget` | `/supermemory-forget ` | Remove a memory. |
-| `/supermemory-login` | `/supermemory-login` | Re-authenticate with Supermemory. |
+| Skill | Usage | Description |
+| ---------------------- | ----------------------------------------------------------- | ---------------------------------------- |
+| `/supermemory-search` | `/supermemory-search [--container ] ` | Search memories manually. |
+| `/supermemory-save` | `/supermemory-save [--container ] ` | Save a specific memory explicitly. |
+| `/supermemory-forget` | `/supermemory-forget [--container ] ` | Remove a memory. |
+| `/supermemory-login` | `/supermemory-login` | Re-authenticate with Supermemory. |
Skills are fallback commands — the hooks handle most use cases automatically.
+## Custom Container Tags
+
+Custom container tags let you organize memories into separate buckets (e.g., `work`,
+`personal`, `code_style`). The AI reads the container descriptions from your config
+and automatically picks the right container when saving memories.
+
+### Setup
+
+Add these fields to `~/.codex/supermemory.json`:
+
+```json
+{
+ "enableCustomContainers": true,
+ "customContainers": [
+ { "tag": "personal", "description": "Personal life — family, health, hobbies, routines" },
+ { "tag": "work", "description": "Work-related — projects, deadlines, meetings, colleagues" },
+ { "tag": "code_style", "description": "Coding preferences — languages, tools, patterns, conventions" }
+ ],
+ "customContainerInstructions": "Route coding preferences to code_style. Personal topics to personal. Default to project container for ambiguous content."
+}
+```
+
+### How it works
+
+1. You define containers with a `tag` (identifier) and a `description` (plain English
+ explaining what belongs there).
+2. On every prompt, the container catalog is injected into the AI's context so it knows
+ what containers are available.
+3. When the AI saves a memory (via `/supermemory-save`), it picks the best matching
+ container based on the descriptions and uses `--container `.
+4. When searching or forgetting, the AI can also target specific containers.
+5. Automatic capture (background saving) always goes to the default project/user
+ containers — only explicit saves get routed to custom containers.
+
+Each container tag automatically becomes a **Space** on the
+[Supermemory dashboard](https://app.supermemory.ai), so you can view and manage
+memories organized by category.
+
+### Container config reference
+
+| Field | Type | Description |
+| ------------------ | -------- | -------------------------------------------------- |
+| `tag` | `string` | Unique identifier for the container (e.g. `work`). |
+| `description` | `string` | Plain English description for AI routing. |
+
## Privacy
Anything wrapped in `...` is replaced with `[REDACTED]` before
diff --git a/src/config.ts b/src/config.ts
index f753d61..25be6ed 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -5,6 +5,11 @@ import { loadCredentials } from "./services/auth.js";
const CONFIG_FILE = join(homedir(), ".codex", "supermemory.json");
+export interface CustomContainer {
+ tag: string;
+ description: string;
+}
+
interface CodexSupermemoryConfig {
apiKey?: string;
similarityThreshold?: number;
@@ -22,6 +27,10 @@ interface CodexSupermemoryConfig {
signalTurnsBefore?: number;
// Auto-save interval
autoSaveEveryTurns?: number;
+ // Custom container routing
+ enableCustomContainers?: boolean;
+ customContainers?: CustomContainer[];
+ customContainerInstructions?: string;
}
const DEFAULT_SIGNAL_KEYWORDS = [
@@ -130,6 +139,13 @@ export const CONFIG = {
signalTurnsBefore: fileConfig.signalTurnsBefore ?? DEFAULTS.signalTurnsBefore,
// Auto-save interval
autoSaveEveryTurns: fileConfig.autoSaveEveryTurns ?? DEFAULTS.autoSaveEveryTurns,
+ // Custom container routing
+ enableCustomContainers: fileConfig.enableCustomContainers ?? false,
+ customContainers: (fileConfig.customContainers ?? []).filter(
+ (c): c is CustomContainer =>
+ !!c && typeof c.tag === "string" && typeof c.description === "string",
+ ),
+ customContainerInstructions: fileConfig.customContainerInstructions ?? "",
};
export function isConfigured(): boolean {
@@ -151,3 +167,49 @@ export function getSignalConfig(): {
turnsBefore: CONFIG.signalTurnsBefore,
};
}
+
+export function getContainerCatalog(): string | null {
+ if (!CONFIG.enableCustomContainers || CONFIG.customContainers.length === 0) {
+ return null;
+ }
+
+ const lines: string[] = [];
+ lines.push("Custom memory containers are available for organizing memories:");
+ lines.push("");
+ for (const c of CONFIG.customContainers) {
+ lines.push(`- \`${c.tag}\`: ${c.description}`);
+ }
+
+ if (CONFIG.customContainerInstructions) {
+ lines.push("");
+ lines.push(CONFIG.customContainerInstructions);
+ }
+
+ lines.push("");
+ lines.push(
+ "When saving memories with /supermemory-save, use --container to route to a specific container.",
+ );
+ lines.push(
+ "When searching with /supermemory-search, use --container to search a specific container.",
+ );
+ lines.push(
+ "When forgetting with /supermemory-forget, use --container to target a specific container.",
+ );
+ lines.push("If no container is specified, memories go to the default project/user containers.");
+
+ return lines.join("\n");
+}
+
+export function validateContainerTag(tag: string): string | null {
+ if (!CONFIG.enableCustomContainers || CONFIG.customContainers.length === 0) {
+ return "Custom containers are not enabled. Remove --container or set enableCustomContainers in config.";
+ }
+
+ const validTags = CONFIG.customContainers.map((c) => c.tag);
+ if (validTags.includes(tag)) {
+ return null;
+ }
+
+ const validList = validTags.map((t) => `'${t}'`).join(", ");
+ return `Unknown container tag '${tag}'. Valid containers: ${validList}`;
+}
diff --git a/src/hooks/recall.ts b/src/hooks/recall.ts
index 5bc6bfa..a248960 100644
--- a/src/hooks/recall.ts
+++ b/src/hooks/recall.ts
@@ -1,7 +1,7 @@
import { readFileSync, existsSync, writeFileSync, unlinkSync, mkdirSync } from "node:fs";
import { join, dirname } from "node:path";
import { homedir } from "node:os";
-import { isConfigured, CONFIG, reloadApiKey } from "../config.js";
+import { isConfigured, CONFIG, reloadApiKey, getContainerCatalog } from "../config.js";
import { SupermemoryClient } from "../services/client.js";
import { getTags } from "../services/tags.js";
import { formatCombinedContext } from "../services/context.js";
@@ -140,9 +140,26 @@ async function main() {
seenCount: seen.size,
});
+ const containerCatalog = getContainerCatalog();
+
if (newFacts.length > 0) {
addSeenFacts(sessionId, newFacts);
- exitWithContext(`[SUPERMEMORY CONTEXT]\n${text}\n[END SUPERMEMORY CONTEXT]`);
+ let additionalContext = `[SUPERMEMORY CONTEXT]\n${text}\n[END SUPERMEMORY CONTEXT]`;
+
+ if (containerCatalog) {
+ additionalContext += `\n\n[SUPERMEMORY CONTAINERS]\n${containerCatalog}\n[END SUPERMEMORY CONTAINERS]`;
+ }
+
+ log("recall: emit context", {
+ additionalContextLength: additionalContext.length,
+ });
+ exitWithContext(additionalContext);
+ } else if (containerCatalog) {
+ const additionalContext = `[SUPERMEMORY CONTAINERS]\n${containerCatalog}\n[END SUPERMEMORY CONTAINERS]`;
+ log("recall: emit container catalog only", {
+ additionalContextLength: additionalContext.length,
+ });
+ exitWithContext(additionalContext);
} else {
exitWithContext("");
}
diff --git a/src/services/capture.ts b/src/services/capture.ts
index 39b9857..d5fb0c0 100644
--- a/src/services/capture.ts
+++ b/src/services/capture.ts
@@ -122,7 +122,12 @@ export async function captureEntries(
});
const transcript = formatTranscript(signalEntries);
- const content = `[Session ${sessionId}]\n${transcript}`;
+ const rawContent = `[Session ${sessionId}]\n${transcript}`;
+
+ const content = rawContent
+ .replace(/\[SUPERMEMORY CONTAINERS\][\s\S]*?\[END SUPERMEMORY CONTAINERS\]\s*/g, "")
+ .replace(/[\s\S]*?<\/supermemory-containers>\s*/g, "")
+ .trim();
const metadata = {
type: "conversation" as const,
diff --git a/src/services/client.ts b/src/services/client.ts
index 36aaa71..3acc06c 100644
--- a/src/services/client.ts
+++ b/src/services/client.ts
@@ -181,7 +181,11 @@ export class SupermemoryClient {
metadata?: { type?: MemoryType; tool?: string; [key: string]: unknown },
options?: { customId?: string }
) {
- log("addMemory: start", { containerTag, contentLength: content.length, customId: options?.customId });
+ log("addMemory: start", {
+ containerTag,
+ contentLength: content.length,
+ customId: options?.customId,
+ });
try {
const payload: {
content: string;
diff --git a/src/skills/forget-memory.ts b/src/skills/forget-memory.ts
index 315561c..301b8cc 100644
--- a/src/skills/forget-memory.ts
+++ b/src/skills/forget-memory.ts
@@ -1,7 +1,22 @@
-import { isConfigured } from "../config.js";
+import { isConfigured, validateContainerTag } from "../config.js";
import { SupermemoryClient } from "../services/client.js";
import { getProjectTag, getUserTag } from "../services/tags.js";
+function parseArgs(args: string[]): { content: string; containerTag?: string } {
+ let containerTag: string | undefined;
+ const contentParts: string[] = [];
+
+ for (let i = 0; i < args.length; i++) {
+ if (args[i] === "--container" && i + 1 < args.length) {
+ containerTag = args[++i];
+ } else {
+ contentParts.push(args[i]);
+ }
+ }
+
+ return { content: contentParts.join(" "), containerTag };
+}
+
async function main(): Promise {
if (!isConfigured()) {
console.error(
@@ -11,45 +26,62 @@ async function main(): Promise {
process.exit(1);
}
- const content = process.argv.slice(2).join(" ");
+ const { content, containerTag } = parseArgs(process.argv.slice(2));
if (!content.trim()) {
console.log(
- 'No content provided. Usage: node forget-memory.js "content to forget"'
+ 'No content provided. Usage: node forget-memory.js [--container ] "content to forget"'
);
process.exit(0);
}
const client = new SupermemoryClient();
- const projectTag = getProjectTag(process.cwd());
- const userTag = getUserTag();
+
+ if (containerTag) {
+ const validationError = validateContainerTag(containerTag);
+ if (validationError) {
+ console.log(validationError);
+ process.exit(1);
+ }
+ }
try {
- // Forget from both project and user scopes since memories may exist in either.
- const [projectResult, userResult] = await Promise.all([
- client.forgetMemory(content, projectTag),
- client.forgetMemory(content, userTag),
- ]);
+ if (containerTag) {
+ const result = await client.forgetMemory(content, containerTag);
+ if (result.success) {
+ console.log(`Memory forgotten from container '${containerTag}'${result.id ? ` (id: ${result.id})` : ""}`);
+ } else {
+ console.log(`Failed to forget memory from container '${containerTag}': ${result.error}`);
+ }
+ } else {
+ const projectTag = getProjectTag(process.cwd());
+ const userTag = getUserTag();
- const forgotten: string[] = [];
- const errors: string[] = [];
+ const [projectResult, userResult] = await Promise.all([
+ client.forgetMemory(content, projectTag),
+ client.forgetMemory(content, userTag),
+ ]);
- if (projectResult.success) {
- forgotten.push(projectResult.id ? `project (id: ${projectResult.id})` : "project");
- } else {
- errors.push(`project: ${projectResult.error}`);
- }
+ const forgotten: string[] = [];
+ const errors: string[] = [];
- if (userResult.success) {
- forgotten.push(userResult.id ? `user (id: ${userResult.id})` : "user");
- } else {
- errors.push(`user: ${userResult.error}`);
- }
+ if (projectResult.success) {
+ forgotten.push(projectResult.id ? `project (id: ${projectResult.id})` : "project");
+ } else {
+ errors.push(`project: ${projectResult.error}`);
+ }
- if (forgotten.length > 0) {
- console.log(`Memory forgotten from: ${forgotten.join(", ")}`);
- } else {
- console.log(`Failed to forget memory: ${errors.join("; ")}`);
+ if (userResult.success) {
+ forgotten.push(userResult.id ? `user (id: ${userResult.id})` : "user");
+ } else {
+ errors.push(`user: ${userResult.error}`);
+ }
+
+ if (forgotten.length > 0) {
+ console.log(`Memory forgotten from: ${forgotten.join(", ")}`);
+ } else {
+ console.log(`Failed to forget memory: ${errors.join("; ")}`);
+ }
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
diff --git a/src/skills/save-memory.ts b/src/skills/save-memory.ts
index 37bd2f1..076e272 100644
--- a/src/skills/save-memory.ts
+++ b/src/skills/save-memory.ts
@@ -1,7 +1,22 @@
-import { isConfigured } from "../config.js";
+import { isConfigured, validateContainerTag } from "../config.js";
import { SupermemoryClient } from "../services/client.js";
import { getProjectTag } from "../services/tags.js";
+function parseArgs(args: string[]): { content: string; containerTag?: string } {
+ let containerTag: string | undefined;
+ const contentParts: string[] = [];
+
+ for (let i = 0; i < args.length; i++) {
+ if (args[i] === "--container" && i + 1 < args.length) {
+ containerTag = args[++i];
+ } else {
+ contentParts.push(args[i]);
+ }
+ }
+
+ return { content: contentParts.join(" "), containerTag };
+}
+
async function main(): Promise {
if (!isConfigured()) {
console.error(
@@ -11,15 +26,23 @@ async function main(): Promise {
process.exit(1);
}
- const content = process.argv.slice(2).join(" ");
+ const { content, containerTag } = parseArgs(process.argv.slice(2));
if (!content.trim()) {
- console.log('No content provided. Usage: node save-memory.js "content to save"');
+ console.log('No content provided. Usage: node save-memory.js [--container ] "content to save"');
process.exit(0);
}
+ if (containerTag) {
+ const validationError = validateContainerTag(containerTag);
+ if (validationError) {
+ console.log(validationError);
+ process.exit(1);
+ }
+ }
+
const client = new SupermemoryClient();
- const projectTag = getProjectTag(process.cwd());
+ const effectiveTag = containerTag || getProjectTag(process.cwd());
try {
const metadata = {
@@ -28,10 +51,11 @@ async function main(): Promise {
timestamp: new Date().toISOString(),
};
- const result = await client.addMemory(content, projectTag, metadata);
+ const result = await client.addMemory(content, effectiveTag, metadata);
if (result.success) {
- console.log(`Memory saved (id: ${result.id}) to project '${projectTag}'`);
+ const tagLabel = containerTag ? `container '${containerTag}'` : `project '${effectiveTag}'`;
+ console.log(`Memory saved (id: ${result.id}) to ${tagLabel}`);
} else {
console.log(`Failed to save memory: ${result.error}`);
}
diff --git a/src/skills/search-memory.ts b/src/skills/search-memory.ts
index b21253c..51fd30d 100644
--- a/src/skills/search-memory.ts
+++ b/src/skills/search-memory.ts
@@ -1,36 +1,41 @@
-import { CONFIG, isConfigured } from "../config.js";
+import { CONFIG, isConfigured, validateContainerTag } from "../config.js";
import { SupermemoryClient, type SearchResponse } from "../services/client.js";
import { formatContextForPrompt } from "../services/context.js";
import { getProjectTag, getUserTag } from "../services/tags.js";
-type Scope = "user" | "project" | "both";
+type Scope = "user" | "project" | "both" | "custom";
interface ParsedArgs {
scope: Scope;
includeProfile: boolean;
query: string;
+ containerTag?: string;
}
function parseArgs(args: string[]): ParsedArgs {
let scope: Scope = "both";
let includeProfile = true;
+ let containerTag: string | undefined;
const queryParts: string[] = [];
- for (const arg of args) {
- if (arg === "--user") {
+ for (let i = 0; i < args.length; i++) {
+ if (args[i] === "--user") {
scope = "user";
- } else if (arg === "--project") {
+ } else if (args[i] === "--project") {
scope = "project";
- } else if (arg === "--both") {
+ } else if (args[i] === "--both") {
scope = "both";
- } else if (arg === "--no-profile") {
+ } else if (args[i] === "--no-profile") {
includeProfile = false;
+ } else if (args[i] === "--container" && i + 1 < args.length) {
+ containerTag = args[++i];
+ scope = "custom";
} else {
- queryParts.push(arg);
+ queryParts.push(args[i]);
}
}
- return { scope, includeProfile, query: queryParts.join(" ") };
+ return { scope, includeProfile, query: queryParts.join(" "), containerTag };
}
async function main(): Promise {
@@ -42,11 +47,11 @@ async function main(): Promise {
process.exit(1);
}
- const { scope, includeProfile, query } = parseArgs(process.argv.slice(2));
+ const { scope, includeProfile, query, containerTag } = parseArgs(process.argv.slice(2));
if (!query.trim()) {
console.log(
- 'No search query provided. Usage: node search-memory.js [--user|--project|--both] "query"'
+ 'No search query provided. Usage: node search-memory.js [--user|--project|--both|--container ] "query"'
);
process.exit(0);
}
@@ -55,10 +60,25 @@ async function main(): Promise {
const userTag = getUserTag();
const projectTag = getProjectTag(process.cwd());
+ if (containerTag) {
+ const validationError = validateContainerTag(containerTag);
+ if (validationError) {
+ console.log(validationError);
+ process.exit(1);
+ }
+ }
+
try {
let searchResult: SearchResponse;
- if (scope === "both") {
+ if (scope === "custom" && containerTag) {
+ searchResult = await client.searchMemories(query, containerTag);
+
+ if (!searchResult.success) {
+ console.log(`Failed to search container '${containerTag}': ${searchResult.error}`);
+ return;
+ }
+ } else if (scope === "both") {
const [userResult, projectResult] = await Promise.all([
client.searchMemories(query, userTag),
client.searchMemories(query, projectTag),
diff --git a/src/skills/supermemory-forget/SKILL.md b/src/skills/supermemory-forget/SKILL.md
index 4c3de07..21efa7a 100644
--- a/src/skills/supermemory-forget/SKILL.md
+++ b/src/skills/supermemory-forget/SKILL.md
@@ -22,6 +22,12 @@ Describe the content to forget — the system will find and remove matching memo
node ~/.codex/supermemory/forget-memory.js "DESCRIPTION_OF_WHAT_TO_FORGET"
```
+To forget from a specific custom container:
+
+```bash
+node ~/.codex/supermemory/forget-memory.js --container "DESCRIPTION_OF_WHAT_TO_FORGET"
+```
+
## Examples
- User says "I no longer use React, I switched to Vue":
diff --git a/src/skills/supermemory-save/SKILL.md b/src/skills/supermemory-save/SKILL.md
index de3e09b..057ff0d 100644
--- a/src/skills/supermemory-save/SKILL.md
+++ b/src/skills/supermemory-save/SKILL.md
@@ -52,3 +52,13 @@ Keep it natural. Capture the conversation flow.
```bash
node ~/.codex/supermemory/save-memory.js "FORMATTED_CONTENT"
```
+
+### Container Routing
+
+If custom containers are configured (see `[SUPERMEMORY CONTAINERS]` in your context), you can route the memory to a specific container using `--container`:
+
+```bash
+node ~/.codex/supermemory/save-memory.js --container "FORMATTED_CONTENT"
+```
+
+Choose the container whose description best matches the content being saved. If unsure, omit `--container` to save to the default project container.
diff --git a/src/skills/supermemory-search/SKILL.md b/src/skills/supermemory-search/SKILL.md
index 0438f1f..6580b8c 100644
--- a/src/skills/supermemory-search/SKILL.md
+++ b/src/skills/supermemory-search/SKILL.md
@@ -13,7 +13,7 @@ Search Supermemory for past coding sessions, decisions, and saved information.
Run the search script with the user's query and optional scope flag:
```bash
-node ~/.codex/supermemory/search-memory.js [--user|--project|--both] "USER_QUERY_HERE"
+node ~/.codex/supermemory/search-memory.js [--user|--project|--both|--container ] "USER_QUERY_HERE"
```
### Scope Flags
@@ -21,6 +21,7 @@ node ~/.codex/supermemory/search-memory.js [--user|--project|--both] "USER_QUERY
- `--both` (default): Search both personal and project memories in parallel
- `--user`: Search personal/user memories across sessions
- `--project`: Search project-specific memories
+- `--container `: Search a specific custom container (see `[SUPERMEMORY CONTAINERS]` in your context for available containers)
### Options