
Everything Claude Code Conventions Skill
- 7k installs
- 227k repo stars
- affaan-m/ecc
everything-claude-code-conventions is an agent skill that enforces consistent Claude Code conventions across prompts, files, and workflows for developers who need standardized agent behavior across projects and sessions.
About
Everything Claude Code Conventions is a comprehensive guide to Claude Code tool usage, skill development patterns, and team conventions. Builders reference it to write skills aligned with ecosystem standards and best practices.
- Claude Code guide
- Best practices
- Conventions reference
Everything Claude Code Conventions by the numbers
- 7,009 all-time installs (skills.sh)
- +5,652 installs in the week ending Aug 11, 2026 (Skillselion tracking)
- Ranked #17 of 793 Skill Development skills by installs in the Skillselion catalog
- Data as of Aug 11, 2026 (Skillselion catalog sync)
npx skills add https://github.com/affaan-m/ecc --skill everything-claude-code-conventionsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 7k |
|---|---|
| repo stars | ★ 227k |
| Repository | affaan-m/ecc ↗ |
How do you standardize Claude Code conventions across projects?
Reference comprehensive Claude Code conventions, best practices, and patterns for skill development.
Who is it for?
Teams using Claude Code across multiple repos who need enforced naming, prompt, and workflow conventions to stop per-developer agent drift.
Skip if: Projects not using Claude Code, or one-off tasks where convention standardization adds no value over direct implementation.
When should I use this skill?
User asks to enforce Claude Code conventions, standardize ECC workflows, audit agent config consistency, or align prompts and file patterns.
What you get
Normalized Claude Code convention rules, aligned prompt and file patterns, and consistent workflow configuration across repositories.
- Convention rule set
- Aligned config patterns
Files
ck — Context Keeper
You are the Context Keeper assistant. When the user invokes any /ck:* command, run the corresponding Node.js script and present its stdout to the user verbatim. Scripts live at: ~/.claude/skills/ck/commands/ (expand ~ with $HOME).
---
Data Layout
~/.claude/ck/
├── projects.json ← path → {name, contextDir, lastUpdated}
└── contexts/<name>/
├── context.json ← SOURCE OF TRUTH (structured JSON, v2)
└── CONTEXT.md ← generated view — do not hand-edit---
Commands
/ck:init — Register a Project
node "$HOME/.claude/skills/ck/commands/init.mjs"The script outputs JSON with auto-detected info. Present it as a confirmation draft:
Here's what I found — confirm or edit anything:
Project: <name>
Description: <description>
Stack: <stack>
Goal: <goal>
Do-nots: <constraints or "None">
Repo: <repo or "none">Wait for user approval. Apply any edits. Then pipe confirmed JSON to save.mjs --init:
echo '<confirmed-json>' | node "$HOME/.claude/skills/ck/commands/save.mjs" --initConfirmed JSON schema: {"name":"...","path":"...","description":"...","stack":["..."],"goal":"...","constraints":["..."],"repo":"..." }
---
/ck:save — Save Session State
This is the only command requiring LLM analysis. Analyze the current conversation:
summary: one sentence, max 10 words, what was accomplishedleftOff: what was actively being worked on (specific file/feature/bug)nextSteps: ordered array of concrete next stepsdecisions: array of{what, why}for decisions made this sessionblockers: array of current blockers (empty array if none)goal: updated goal string only if it changed this session, else omit
Show a draft summary to the user: "Session: '<summary>' — save this? (yes / edit)" Wait for confirmation. Then pipe to save.mjs:
echo '<json>' | node "$HOME/.claude/skills/ck/commands/save.mjs"JSON schema (exact): {"summary":"...","leftOff":"...","nextSteps":["..."],"decisions":[{"what":"...","why":"..."}],"blockers":["..."]} Display the script's stdout confirmation verbatim.
---
/ck:resume [name|number] — Full Briefing
node "$HOME/.claude/skills/ck/commands/resume.mjs" [arg]Display output verbatim. Then ask: "Continue from here? Or has anything changed?" If user reports changes → run /ck:save immediately.
---
/ck:info [name|number] — Quick Snapshot
node "$HOME/.claude/skills/ck/commands/info.mjs" [arg]Display output verbatim. No follow-up question.
---
/ck:list — Portfolio View
node "$HOME/.claude/skills/ck/commands/list.mjs"Display output verbatim. If user replies with a number or name → run /ck:resume.
---
/ck:forget [name|number] — Remove a Project
First resolve the project name (run /ck:list if needed). Ask: "This will permanently delete context for '<name>'. Are you sure? (yes/no)" If yes:
node "$HOME/.claude/skills/ck/commands/forget.mjs" [name]Display confirmation verbatim.
---
/ck:migrate — Convert v1 Data to v2
node "$HOME/.claude/skills/ck/commands/migrate.mjs"For a dry run first:
node "$HOME/.claude/skills/ck/commands/migrate.mjs" --dry-runDisplay output verbatim. Migrates all v1 CONTEXT.md + meta.json files to v2 context.json. Originals are backed up as meta.json.v1-backup — nothing is deleted.
---
SessionStart Hook
The hook at ~/.claude/skills/ck/hooks/session-start.mjs must be registered in ~/.claude/settings.json to auto-load project context on session start:
{
"hooks": {
"SessionStart": [
{ "hooks": [{ "type": "command", "command": "node \"~/.claude/skills/ck/hooks/session-start.mjs\"" }] }
]
}
}The hook injects ~100 tokens per session (compact 5-line summary). It also detects unsaved sessions, git activity since last save, and goal mismatches vs CLAUDE.md.
---
Rules
- Always expand
~as$HOMEin Bash calls. - Commands are case-insensitive:
/CK:SAVE,/ck:save,/Ck:Saveall work. - If a script exits with code 1, display its stdout as an error message.
- Never edit
context.jsonorCONTEXT.mddirectly — always use the scripts. - If
projects.jsonis malformed, tell the user and offer to reset it to{}.
#!/usr/bin/env node
/**
* ck — Context Keeper v2
* forget.mjs — remove a project's context and registry entry
*
* Usage: node forget.mjs [name|number]
* stdout: confirmation or error
* exit 0: success exit 1: not found
*
* Note: SKILL.md instructs Claude to ask "Are you sure?" before calling this script.
* This script is the "do it" step — no confirmation prompt here.
*/
import { rmSync } from 'fs';
import { resolve } from 'path';
import { resolveContext, readProjects, writeProjects, CONTEXTS_DIR } from './shared.mjs';
const arg = process.argv[2];
const cwd = process.env.PWD || process.cwd();
const resolved = resolveContext(arg, cwd);
if (!resolved) {
const hint = arg ? `No project matching "${arg}".` : 'This directory is not registered.';
console.log(`${hint}`);
process.exit(1);
}
const { name, contextDir, projectPath } = resolved;
// Remove context directory
const contextDirPath = resolve(CONTEXTS_DIR, contextDir);
try {
rmSync(contextDirPath, { recursive: true, force: true });
} catch (e) {
console.log(`ck: could not remove context directory — ${e.message}`);
process.exit(1);
}
// Remove from projects.json
const projects = readProjects();
delete projects[projectPath];
writeProjects(projects);
console.log(`✓ Context for '${name}' removed.`);
#!/usr/bin/env node
/**
* ck — Context Keeper v2
* info.mjs — quick read-only context snapshot
*
* Usage: node info.mjs [name|number]
* stdout: compact info block
* exit 0: success exit 1: not found
*/
import { resolveContext, renderInfoBlock } from './shared.mjs';
const arg = process.argv[2];
const cwd = process.env.PWD || process.cwd();
const resolved = resolveContext(arg, cwd);
if (!resolved) {
const hint = arg ? `No project matching "${arg}".` : 'This directory is not registered.';
console.log(`${hint} Run /ck:init to register it.`);
process.exit(1);
}
console.log('');
console.log(renderInfoBlock(resolved.context));
#!/usr/bin/env node
/**
* ck — Context Keeper v2
* init.mjs — auto-detect project info and output JSON for Claude to confirm
*
* Usage: node init.mjs
* stdout: JSON with auto-detected project info
* exit 0: success exit 1: error
*/
import { readFileSync, existsSync } from 'fs';
import { resolve, basename } from 'path';
import { readProjects } from './shared.mjs';
const cwd = process.env.PWD || process.cwd();
const projects = readProjects();
const output = {
path: cwd,
name: null,
description: null,
stack: [],
goal: null,
constraints: [],
repo: null,
alreadyRegistered: !!projects[cwd],
};
function readFile(filename) {
const p = resolve(cwd, filename);
if (!existsSync(p)) return null;
try { return readFileSync(p, 'utf8'); } catch { return null; }
}
function extractSection(md, heading) {
const re = new RegExp(`## ${heading}\\n([\\s\\S]*?)(?=\\n## |$)`);
const m = md.match(re);
return m ? m[1].trim() : null;
}
// ── package.json ──────────────────────────────────────────────────────────────
const pkg = readFile('package.json');
if (pkg) {
try {
const parsed = JSON.parse(pkg);
if (parsed.name && !output.name) output.name = parsed.name;
if (parsed.description && !output.description) output.description = parsed.description;
// Detect stack from dependencies
const deps = Object.keys({ ...(parsed.dependencies || {}), ...(parsed.devDependencies || {}) });
const stackMap = {
next: 'Next.js', react: 'React', vue: 'Vue', svelte: 'Svelte', astro: 'Astro',
express: 'Express', fastify: 'Fastify', hono: 'Hono', nestjs: 'NestJS',
typescript: 'TypeScript', prisma: 'Prisma', drizzle: 'Drizzle',
'@neondatabase/serverless': 'Neon', '@upstash/redis': 'Upstash Redis',
'@clerk/nextjs': 'Clerk', stripe: 'Stripe', tailwindcss: 'Tailwind CSS',
};
for (const [dep, label] of Object.entries(stackMap)) {
if (deps.includes(dep) && !output.stack.includes(label)) {
output.stack.push(label);
}
}
if (deps.includes('typescript') || existsSync(resolve(cwd, 'tsconfig.json'))) {
if (!output.stack.includes('TypeScript')) output.stack.push('TypeScript');
}
} catch { /* malformed package.json */ }
}
// ── go.mod ────────────────────────────────────────────────────────────────────
const goMod = readFile('go.mod');
if (goMod) {
if (!output.stack.includes('Go')) output.stack.push('Go');
const modName = goMod.match(/^module\s+(\S+)/m)?.[1];
if (modName && !output.name) output.name = modName.split('/').pop();
}
// ── Cargo.toml ────────────────────────────────────────────────────────────────
const cargo = readFile('Cargo.toml');
if (cargo) {
if (!output.stack.includes('Rust')) output.stack.push('Rust');
const crateName = cargo.match(/^name\s*=\s*"(.+?)"/m)?.[1];
if (crateName && !output.name) output.name = crateName;
}
// ── pyproject.toml ────────────────────────────────────────────────────────────
const pyproject = readFile('pyproject.toml');
if (pyproject) {
if (!output.stack.includes('Python')) output.stack.push('Python');
const pyName = pyproject.match(/^name\s*=\s*"(.+?)"/m)?.[1];
if (pyName && !output.name) output.name = pyName;
}
// ── .git/config (repo URL) ────────────────────────────────────────────────────
const gitConfig = readFile('.git/config');
if (gitConfig) {
const repoMatch = gitConfig.match(/url\s*=\s*(.+)/);
if (repoMatch) output.repo = repoMatch[1].trim();
}
// ── CLAUDE.md ─────────────────────────────────────────────────────────────────
const claudeMd = readFile('CLAUDE.md');
if (claudeMd) {
const goal = extractSection(claudeMd, 'Current Goal');
if (goal && !output.goal) output.goal = goal.split('\n')[0].trim();
const doNot = extractSection(claudeMd, 'Do Not Do');
if (doNot) {
const bullets = doNot.split('\n')
.filter(l => /^[-*]\s+/.test(l))
.map(l => l.replace(/^[-*]\s+/, '').trim());
output.constraints = bullets;
}
const stack = extractSection(claudeMd, 'Tech Stack');
if (stack && output.stack.length === 0) {
output.stack = stack.split(/[,\n]/).map(s => s.replace(/^[-*]\s+/, '').trim()).filter(Boolean);
}
// Description from first section or "What This Is"
const whatItIs = extractSection(claudeMd, 'What This Is') || extractSection(claudeMd, 'About');
if (whatItIs && !output.description) output.description = whatItIs.split('\n')[0].trim();
}
// ── README.md (description fallback) ─────────────────────────────────────────
const readme = readFile('README.md');
if (readme && !output.description) {
// First non-header, non-badge, non-empty paragraph
const lines = readme.split('\n');
for (const line of lines) {
const trimmed = line.trim();
if (trimmed && !trimmed.startsWith('#') && !trimmed.startsWith('!') && !trimmed.startsWith('>') && !trimmed.startsWith('[') && trimmed !== '---' && trimmed !== '___') {
output.description = trimmed.slice(0, 120);
break;
}
}
}
// ── Name fallback: directory name ─────────────────────────────────────────────
if (!output.name) {
output.name = basename(cwd).toLowerCase().replace(/\s+/g, '-');
}
console.log(JSON.stringify(output, null, 2));
#!/usr/bin/env node
/**
* ck — Context Keeper v2
* list.mjs — portfolio view of all registered projects
*
* Usage: node list.mjs
* stdout: ASCII table of all projects + prompt to resume
* exit 0: success exit 1: no projects
*/
import { readProjects, loadContext, today, renderListTable } from './shared.mjs';
const cwd = process.env.PWD || process.cwd();
const projects = readProjects();
const entries = Object.entries(projects);
if (entries.length === 0) {
console.log('No projects registered. Run /ck:init to get started.');
process.exit(1);
}
// Build enriched list sorted alphabetically by contextDir
const enriched = entries
.map(([path, info]) => {
const context = loadContext(info.contextDir);
return {
name: info.name,
contextDir: info.contextDir,
path,
context,
lastUpdated: info.lastUpdated,
};
})
.sort((a, b) => a.contextDir.localeCompare(b.contextDir));
const table = renderListTable(enriched, cwd, today());
console.log('');
console.log(table);
console.log('');
console.log('Resume which? (number or name)');
#!/usr/bin/env node
/**
* ck — Context Keeper v2
* migrate.mjs — convert v1 (CONTEXT.md + meta.json) to v2 (context.json)
*
* Usage:
* node migrate.mjs — migrate all v1 projects
* node migrate.mjs --dry-run — preview without writing
*
* Safe: backs up meta.json to meta.json.v1-backup, never deletes data.
* exit 0: success exit 1: error
*/
import { readFileSync, existsSync, renameSync } from 'fs';
import { resolve } from 'path';
import { readProjects, writeProjects, saveContext, today, shortId, CONTEXTS_DIR } from './shared.mjs';
const isDryRun = process.argv.includes('--dry-run');
if (isDryRun) {
console.log('ck migrate — DRY RUN (no files will be written)\n');
}
// ── v1 markdown parsers ───────────────────────────────────────────────────────
function extractSection(md, heading) {
const re = new RegExp(`## ${heading}\\n([\\s\\S]*?)(?=\\n## |$)`);
const m = md.match(re);
return m ? m[1].trim() : null;
}
function parseBullets(text) {
if (!text) return [];
return text.split('\n')
.filter(l => /^[-*\d]\s/.test(l.trim()))
.map(l => l.replace(/^[-*\d]+\.?\s+/, '').trim())
.filter(Boolean);
}
function parseDecisionsTable(text) {
if (!text) return [];
const rows = [];
for (const line of text.split('\n')) {
if (!line.startsWith('|') || line.match(/^[|\s-]+$/)) continue;
const cols = line.split('|').map(c => c.trim()).filter((c, i) => i > 0 && i < 4);
if (cols.length >= 1 && !cols[0].startsWith('Decision') && !cols[0].startsWith('_')) {
rows.push({ what: cols[0] || '', why: cols[1] || '', date: cols[2] || '' });
}
}
return rows;
}
/**
* Parse "Where I Left Off" which in v1 can be:
* - Simple bullet list
* - Multi-session blocks: "Session N (date):\n- bullet\n"
* Returns array of session-like objects {date?, leftOff}
*/
function parseLeftOff(text) {
if (!text) return [{ leftOff: null }];
// Detect multi-session format: "Session N ..."
const sessionBlocks = text.split(/(?=Session \d+)/);
if (sessionBlocks.length > 1) {
return sessionBlocks
.filter(b => b.trim())
.map(block => {
const dateMatch = block.match(/\((\d{4}-\d{2}-\d{2})\)/);
const bullets = parseBullets(block);
return {
date: dateMatch?.[1] || null,
leftOff: bullets.length ? bullets.join('\n') : block.replace(/^Session \d+.*\n/, '').trim(),
};
});
}
// Simple format
const bullets = parseBullets(text);
return [{ leftOff: bullets.length ? bullets.join('\n') : text.trim() }];
}
// ── Main migration ─────────────────────────────────────────────────────────────
const projects = readProjects();
let migrated = 0;
let skipped = 0;
let errors = 0;
for (const [projectPath, info] of Object.entries(projects)) {
const contextDir = info.contextDir;
const contextDirPath = resolve(CONTEXTS_DIR, contextDir);
const contextJsonPath = resolve(contextDirPath, 'context.json');
const contextMdPath = resolve(contextDirPath, 'CONTEXT.md');
const metaPath = resolve(contextDirPath, 'meta.json');
// Already v2
if (existsSync(contextJsonPath)) {
try {
const existing = JSON.parse(readFileSync(contextJsonPath, 'utf8'));
if (existing.version === 2) {
console.log(` ✓ ${contextDir} — already v2, skipping`);
skipped++;
continue;
}
} catch { /* fall through to migrate */ }
}
console.log(`\n → Migrating: ${contextDir}`);
try {
// Read v1 files
const contextMd = existsSync(contextMdPath) ? readFileSync(contextMdPath, 'utf8') : '';
let meta = {};
if (existsSync(metaPath)) {
try {
meta = JSON.parse(readFileSync(metaPath, 'utf8'));
} catch (e) {
console.warn(` ! ${contextDir}: invalid meta.json, continuing with defaults (${e.message})`);
}
}
// Extract fields from CONTEXT.md
const description = extractSection(contextMd, 'What This Is') || extractSection(contextMd, 'About') || null;
const stackRaw = extractSection(contextMd, 'Tech Stack') || '';
const stack = stackRaw.split(/[,\n]/).map(s => s.replace(/^[-*]\s+/, '').trim()).filter(Boolean);
const goal = (extractSection(contextMd, 'Current Goal') || '').split('\n')[0].trim() || null;
const constraintRaw = extractSection(contextMd, 'Do Not Do') || '';
const constraints = parseBullets(constraintRaw);
const decisionsRaw = extractSection(contextMd, 'Decisions Made') || '';
const decisions = parseDecisionsTable(decisionsRaw);
const nextStepsRaw = extractSection(contextMd, 'Next Steps') || '';
const nextSteps = parseBullets(nextStepsRaw);
const blockersRaw = extractSection(contextMd, 'Blockers') || '';
const blockers = parseBullets(blockersRaw).filter(b => b.toLowerCase() !== 'none');
const leftOffRaw = extractSection(contextMd, 'Where I Left Off') || '';
const leftOffParsed = parseLeftOff(leftOffRaw);
// Build sessions from parsed left-off blocks (may be multiple)
const sessions = leftOffParsed.map((lo, idx) => ({
id: idx === leftOffParsed.length - 1 && meta.lastSessionId
? meta.lastSessionId.slice(0, 8)
: shortId(),
date: lo.date || meta.lastUpdated || today(),
summary: idx === leftOffParsed.length - 1
? (meta.lastSessionSummary || 'Migrated from v1')
: `Session ${idx + 1} (migrated)`,
leftOff: lo.leftOff,
nextSteps: idx === leftOffParsed.length - 1 ? nextSteps : [],
decisions: idx === leftOffParsed.length - 1 ? decisions : [],
blockers: idx === leftOffParsed.length - 1 ? blockers : [],
}));
const context = {
version: 2,
name: contextDir,
path: meta.path || projectPath,
description,
stack,
goal,
constraints,
repo: meta.repo || null,
createdAt: meta.lastUpdated || today(),
sessions,
};
if (isDryRun) {
console.log(` description: ${description?.slice(0, 60) || '(none)'}`);
console.log(` stack: ${stack.join(', ') || '(none)'}`);
console.log(` goal: ${goal?.slice(0, 60) || '(none)'}`);
console.log(` sessions: ${sessions.length}`);
console.log(` decisions: ${decisions.length}`);
console.log(` nextSteps: ${nextSteps.length}`);
migrated++;
continue;
}
// Backup meta.json
if (existsSync(metaPath)) {
renameSync(metaPath, resolve(contextDirPath, 'meta.json.v1-backup'));
}
// Write context.json + regenerated CONTEXT.md
saveContext(contextDir, context);
// Update projects.json entry
projects[projectPath].lastUpdated = today();
console.log(` ✓ Migrated — ${sessions.length} session(s), ${decisions.length} decision(s)`);
migrated++;
} catch (e) {
console.log(` ✗ Error: ${e.message}`);
errors++;
}
}
if (!isDryRun && migrated > 0) {
writeProjects(projects);
}
console.log(`\nck migrate: ${migrated} migrated, ${skipped} already v2, ${errors} errors`);
if (isDryRun) console.log('Run without --dry-run to apply.');
if (errors > 0) process.exit(1);
#!/usr/bin/env node
/**
* ck — Context Keeper v2
* resume.mjs — full project briefing
*
* Usage: node resume.mjs [name|number]
* stdout: bordered briefing box
* exit 0: success exit 1: not found
*/
import { existsSync } from 'fs';
import { resolveContext, renderBriefingBox } from './shared.mjs';
const arg = process.argv[2];
const cwd = process.env.PWD || process.cwd();
const resolved = resolveContext(arg, cwd);
if (!resolved) {
const hint = arg ? `No project matching "${arg}".` : 'This directory is not registered.';
console.log(`${hint} Run /ck:init to register it.`);
process.exit(1);
}
const { context, projectPath } = resolved;
// Attempt to cd to the project path
if (projectPath && projectPath !== cwd) {
if (existsSync(projectPath)) {
console.log(`→ cd ${projectPath}`);
} else {
console.log(`WARNING Path not found: ${projectPath}`);
}
}
console.log('');
console.log(renderBriefingBox(context));
#!/usr/bin/env node
/**
* ck — Context Keeper v2
* save.mjs — write session data to context.json, regenerate CONTEXT.md,
* and write a native memory entry.
*
* Usage (regular save):
* echo '<json>' | node save.mjs
* JSON schema: { summary, leftOff, nextSteps[], decisions[{what,why}], blockers[], goal? }
*
* Usage (init — first registration):
* echo '<json>' | node save.mjs --init
* JSON schema: { name, path, description, stack[], goal, constraints[], repo? }
*
* stdout: confirmation message
* exit 0: success exit 1: error
*/
import { readFileSync, mkdirSync, writeFileSync } from 'fs';
import { resolve } from 'path';
import {
readProjects, writeProjects, loadContext, saveContext,
today, shortId, gitSummary, nativeMemoryDir,
CURRENT_SESSION,
} from './shared.mjs';
const isInit = process.argv.includes('--init');
const cwd = process.env.PWD || process.cwd();
// ── Read JSON from stdin ──────────────────────────────────────────────────────
let input;
try {
const raw = readFileSync(0, 'utf8').trim();
if (!raw) throw new Error('empty stdin');
input = JSON.parse(raw);
} catch (e) {
console.error(`ck save: invalid JSON on stdin — ${e.message}`);
console.log('Expected schema (save): {"summary":"...","leftOff":"...","nextSteps":["..."],"decisions":[{"what":"...","why":"..."}],"blockers":["..."]}');
console.log('Expected schema (--init): {"name":"...","path":"...","description":"...","stack":["..."],"goal":"...","constraints":["..."]}');
process.exit(1);
}
// ─────────────────────────────────────────────────────────────────────────────
// INIT MODE: first-time project registration
// ─────────────────────────────────────────────────────────────────────────────
if (isInit) {
const { name, path: projectPath, description, stack, goal, constraints, repo } = input;
if (!name || !projectPath) {
console.log('ck init: name and path are required.');
process.exit(1);
}
const projects = readProjects();
// Derive contextDir (lowercase, spaces→dashes, deduplicate)
let contextDir = name.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
let suffix = 2;
const existingDirs = Object.values(projects).map(p => p.contextDir);
while (existingDirs.includes(contextDir) && projects[projectPath]?.contextDir !== contextDir) {
contextDir = `${contextDir.replace(/-\d+$/, '')}-${suffix++}`;
}
const context = {
version: 2,
name: contextDir,
displayName: name,
path: projectPath,
description: description || null,
stack: Array.isArray(stack) ? stack : (stack ? [stack] : []),
goal: goal || null,
constraints: Array.isArray(constraints) ? constraints : [],
repo: repo || null,
createdAt: today(),
sessions: [],
};
saveContext(contextDir, context);
// Update projects.json
projects[projectPath] = {
name,
contextDir,
lastUpdated: today(),
};
writeProjects(projects);
console.log(`✓ Project '${name}' registered.`);
console.log(` Use /ck:save to save session state and /ck:resume to reload it next time.`);
process.exit(0);
}
// ─────────────────────────────────────────────────────────────────────────────
// SAVE MODE: record a session
// ─────────────────────────────────────────────────────────────────────────────
const projects = readProjects();
const projectEntry = projects[cwd];
if (!projectEntry) {
console.log("This project isn't registered yet. Run /ck:init first.");
process.exit(1);
}
const { contextDir } = projectEntry;
let context = loadContext(contextDir);
if (!context) {
console.log(`ck: context.json not found for '${contextDir}'. The install may be corrupted.`);
process.exit(1);
}
// Get session ID from current-session.json
let sessionId;
try {
const sess = JSON.parse(readFileSync(CURRENT_SESSION, 'utf8'));
sessionId = sess.sessionId || shortId();
} catch {
sessionId = shortId();
}
// Check for duplicate (re-save of same session)
const existingIdx = context.sessions.findIndex(s => s.id === sessionId);
const { summary, leftOff, nextSteps, decisions, blockers, goal } = input;
// Capture git activity since the last session
const lastSessionDate = context.sessions?.[context.sessions.length - 1]?.date;
const gitActivity = gitSummary(cwd, lastSessionDate);
const session = {
id: sessionId,
date: today(),
summary: summary || 'Session saved',
leftOff: leftOff || null,
nextSteps: Array.isArray(nextSteps) ? nextSteps : (nextSteps ? [nextSteps] : []),
decisions: Array.isArray(decisions) ? decisions : [],
blockers: Array.isArray(blockers) ? blockers.filter(Boolean) : [],
...(gitActivity ? { gitActivity } : {}),
};
if (existingIdx >= 0) {
// Update existing session (re-save)
context.sessions[existingIdx] = session;
} else {
context.sessions.push(session);
}
// Update goal if provided
if (goal && goal !== context.goal) {
context.goal = goal;
}
// Save context.json + regenerate CONTEXT.md
saveContext(contextDir, context);
// Update projects.json timestamp
projects[cwd].lastUpdated = today();
writeProjects(projects);
// ── Write to native memory ────────────────────────────────────────────────────
try {
const memDir = nativeMemoryDir(cwd);
mkdirSync(memDir, { recursive: true });
const memFile = resolve(memDir, `ck_${today()}_${sessionId.slice(0, 8)}.md`);
const decisionsBlock = session.decisions.length
? session.decisions.map(d => `- **${d.what}**: ${d.why || ''}`).join('\n')
: '- None this session';
const nextBlock = session.nextSteps.length
? session.nextSteps.map((s, i) => `${i + 1}. ${s}`).join('\n')
: '- None recorded';
const blockersBlock = session.blockers.length
? session.blockers.map(b => `- ${b}`).join('\n')
: '- None';
const memContent = [
`---`,
`name: Session ${today()} — ${session.summary}`,
`description: Key decisions and outcomes from ck session ${sessionId.slice(0, 8)}`,
`type: project`,
`source: ck`,
`sessionId: ${sessionId}`,
`---`,
``,
`# Session: ${session.summary}`,
``,
`## Decisions`,
decisionsBlock,
``,
`## Left Off`,
session.leftOff || '—',
``,
`## Next Steps`,
nextBlock,
``,
`## Blockers`,
blockersBlock,
``,
...(gitActivity ? [`## Git Activity`, gitActivity, ``] : []),
].join('\n');
writeFileSync(memFile, memContent, 'utf8');
} catch (e) {
// Non-fatal — native memory write failure should not block the save
process.stderr.write(`ck: warning — could not write native memory entry: ${e.message}\n`);
}
console.log(`✓ Saved. Session: ${sessionId.slice(0, 8)}`);
if (gitActivity) console.log(` Git: ${gitActivity}`);
console.log(` See you next time.`);
/**
* ck — Context Keeper v2
* shared.mjs — common utilities for all command scripts
*
* No external dependencies. Node.js stdlib only.
*/
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs';
import { resolve } from 'path';
import { homedir } from 'os';
import { spawnSync } from 'child_process';
import { randomBytes } from 'crypto';
// ─── Paths ────────────────────────────────────────────────────────────────────
export const CK_HOME = resolve(homedir(), '.claude', 'ck');
export const CONTEXTS_DIR = resolve(CK_HOME, 'contexts');
export const PROJECTS_FILE = resolve(CK_HOME, 'projects.json');
export const CURRENT_SESSION = resolve(CK_HOME, 'current-session.json');
export const SKILL_FILE = resolve(homedir(), '.claude', 'skills', 'ck', 'SKILL.md');
// ─── JSON I/O ─────────────────────────────────────────────────────────────────
export function readJson(filePath) {
try {
if (!existsSync(filePath)) return null;
return JSON.parse(readFileSync(filePath, 'utf8'));
} catch {
return null;
}
}
export function writeJson(filePath, data) {
const dir = resolve(filePath, '..');
mkdirSync(dir, { recursive: true });
writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf8');
}
export function readProjects() {
return readJson(PROJECTS_FILE) || {};
}
export function writeProjects(projects) {
writeJson(PROJECTS_FILE, projects);
}
// ─── Context I/O ──────────────────────────────────────────────────────────────
export function contextPath(contextDir) {
return resolve(CONTEXTS_DIR, contextDir, 'context.json');
}
export function contextMdPath(contextDir) {
return resolve(CONTEXTS_DIR, contextDir, 'CONTEXT.md');
}
export function loadContext(contextDir) {
return readJson(contextPath(contextDir));
}
export function saveContext(contextDir, data) {
const dir = resolve(CONTEXTS_DIR, contextDir);
mkdirSync(dir, { recursive: true });
writeJson(contextPath(contextDir), data);
writeFileSync(contextMdPath(contextDir), renderContextMd(data), 'utf8');
}
/**
* Resolve which project to operate on.
* @param {string|undefined} arg — undefined = cwd match, number string = alphabetical index, else name search
* @param {string} cwd
* @returns {{ name, contextDir, projectPath, context } | null}
*/
export function resolveContext(arg, cwd) {
const projects = readProjects();
const entries = Object.entries(projects); // [path, {name, contextDir, lastUpdated}]
if (!arg) {
// Match by cwd
const entry = projects[cwd];
if (!entry) return null;
const context = loadContext(entry.contextDir);
if (!context) return null;
return { name: entry.name, contextDir: entry.contextDir, projectPath: cwd, context };
}
// Collect all contexts sorted alphabetically by contextDir
const sorted = entries
.map(([path, info]) => ({ path, ...info }))
.sort((a, b) => a.contextDir.localeCompare(b.contextDir));
const asNumber = parseInt(arg, 10);
if (!isNaN(asNumber) && String(asNumber) === arg) {
// Number-based lookup (1-indexed)
const item = sorted[asNumber - 1];
if (!item) return null;
const context = loadContext(item.contextDir);
if (!context) return null;
return { name: item.name, contextDir: item.contextDir, projectPath: item.path, context };
}
// Name-based lookup: exact > prefix > substring (case-insensitive)
const lower = arg.toLowerCase();
let match =
sorted.find(e => e.name.toLowerCase() === lower) ||
sorted.find(e => e.name.toLowerCase().startsWith(lower)) ||
sorted.find(e => e.name.toLowerCase().includes(lower));
if (!match) return null;
const context = loadContext(match.contextDir);
if (!context) return null;
return { name: match.name, contextDir: match.contextDir, projectPath: match.path, context };
}
// ─── Date helpers ─────────────────────────────────────────────────────────────
export function today() {
return new Date().toISOString().slice(0, 10);
}
export function daysAgoLabel(dateStr) {
if (!dateStr) return 'unknown';
const diff = Math.floor((Date.now() - new Date(dateStr)) / 86_400_000);
if (diff === 0) return 'Today';
if (diff === 1) return '1 day ago';
return `${diff} days ago`;
}
export function stalenessIcon(dateStr) {
if (!dateStr) return '○';
const diff = Math.floor((Date.now() - new Date(dateStr)) / 86_400_000);
if (diff < 1) return '●';
if (diff <= 5) return '◐';
return '○';
}
// ─── ID generation ────────────────────────────────────────────────────────────
export function shortId() {
return randomBytes(4).toString('hex');
}
// ─── Git helpers ──────────────────────────────────────────────────────────────
function runGit(args, cwd) {
try {
const result = spawnSync('git', ['-C', cwd, ...args], {
timeout: 3000,
stdio: 'pipe',
encoding: 'utf8',
});
if (result.status !== 0) return null;
return result.stdout.trim();
} catch {
return null;
}
}
export function gitLogSince(projectPath, sinceDate) {
if (!sinceDate) return null;
return runGit(['log', '--oneline', `--since=${sinceDate}`], projectPath);
}
export function gitSummary(projectPath, sinceDate) {
const log = gitLogSince(projectPath, sinceDate);
if (!log) return null;
const commits = log.split('\n').filter(Boolean).length;
if (commits === 0) return null;
// Count unique files changed: use a separate runGit call to avoid nested shell substitution
const countStr = runGit(['rev-list', '--count', 'HEAD', `--since=${sinceDate}`], projectPath);
const revCount = countStr ? parseInt(countStr, 10) : commits;
const diff = runGit(['diff', '--shortstat', `HEAD~${Math.min(revCount, 50)}..HEAD`], projectPath);
if (diff) {
const filesMatch = diff.match(/(\d+) file/);
const files = filesMatch ? parseInt(filesMatch[1]) : '?';
return `${commits} commit${commits !== 1 ? 's' : ''}, ${files} file${files !== 1 ? 's' : ''} changed`;
}
return `${commits} commit${commits !== 1 ? 's' : ''}`;
}
// ─── Native memory path encoding ──────────────────────────────────────────────
export function encodeProjectPath(absolutePath) {
// "/Users/sree/dev/app" -> "-Users-sree-dev-app"
return absolutePath.replace(/\//g, '-');
}
export function nativeMemoryDir(absolutePath) {
const encoded = encodeProjectPath(absolutePath);
return resolve(homedir(), '.claude', 'projects', encoded, 'memory');
}
// ─── Rendering ────────────────────────────────────────────────────────────────
/** Render the human-readable CONTEXT.md from context.json */
export function renderContextMd(ctx) {
const latest = ctx.sessions?.[ctx.sessions.length - 1] || null;
const lines = [
`<!-- Generated by ck v2 — edit context.json instead -->`,
`# Project: ${ctx.displayName ?? ctx.name}`,
`> Path: ${ctx.path}`,
];
if (ctx.repo) lines.push(`> Repo: ${ctx.repo}`);
const sessionCount = ctx.sessions?.length || 0;
lines.push(`> Last Session: ${ctx.sessions?.[sessionCount - 1]?.date || 'never'} | Sessions: ${sessionCount}`);
lines.push(``);
lines.push(`## What This Is`);
lines.push(ctx.description || '_Not set._');
lines.push(``);
lines.push(`## Tech Stack`);
lines.push(Array.isArray(ctx.stack) ? ctx.stack.join(', ') : (ctx.stack || '_Not set._'));
lines.push(``);
lines.push(`## Current Goal`);
lines.push(ctx.goal || '_Not set._');
lines.push(``);
lines.push(`## Where I Left Off`);
lines.push(latest?.leftOff || '_Not yet recorded. Run /ck:save after your first session._');
lines.push(``);
lines.push(`## Next Steps`);
if (latest?.nextSteps?.length) {
latest.nextSteps.forEach((s, i) => lines.push(`${i + 1}. ${s}`));
} else {
lines.push(`_Not yet recorded._`);
}
lines.push(``);
lines.push(`## Blockers`);
if (latest?.blockers?.length) {
latest.blockers.forEach(b => lines.push(`- ${b}`));
} else {
lines.push(`- None`);
}
lines.push(``);
lines.push(`## Do Not Do`);
if (ctx.constraints?.length) {
ctx.constraints.forEach(c => lines.push(`- ${c}`));
} else {
lines.push(`- None specified`);
}
lines.push(``);
// All decisions across sessions
const allDecisions = (ctx.sessions || []).flatMap(s =>
(s.decisions || []).map(d => ({ ...d, date: s.date }))
);
lines.push(`## Decisions Made`);
lines.push(`| Decision | Why | Date |`);
lines.push(`|----------|-----|------|`);
if (allDecisions.length) {
allDecisions.forEach(d => lines.push(`| ${d.what} | ${d.why || ''} | ${d.date || ''} |`));
} else {
lines.push(`| _(none yet)_ | | |`);
}
lines.push(``);
// Session history (most recent first)
if (ctx.sessions?.length > 1) {
lines.push(`## Session History`);
const reversed = [...ctx.sessions].reverse();
reversed.forEach(s => {
lines.push(`### ${s.date} — ${s.summary || 'Session'}`);
if (s.gitActivity) lines.push(`_${s.gitActivity}_`);
if (s.leftOff) lines.push(`**Left off:** ${s.leftOff}`);
});
lines.push(``);
}
return lines.join('\n');
}
/** Render the bordered briefing box used by /ck:resume */
export function renderBriefingBox(ctx, _meta = {}) {
const latest = ctx.sessions?.[ctx.sessions.length - 1] || {};
const W = 57;
const pad = (str, w) => {
const s = String(str || '');
return s.length > w ? s.slice(0, w - 1) + '…' : s.padEnd(w);
};
const row = (label, value) => `│ ${label} → ${pad(value, W - label.length - 7)}│`;
const when = daysAgoLabel(ctx.sessions?.[ctx.sessions.length - 1]?.date);
const sessions = ctx.sessions?.length || 0;
const shortSessId = latest.id?.slice(0, 8) || null;
const lines = [
`┌${'─'.repeat(W)}┐`,
`│ RESUMING: ${pad(ctx.displayName ?? ctx.name, W - 12)}│`,
`│ Last session: ${pad(`${when} | Sessions: ${sessions}`, W - 16)}│`,
];
if (shortSessId) lines.push(`│ Session ID: ${pad(shortSessId, W - 14)}│`);
lines.push(`├${'─'.repeat(W)}┤`);
lines.push(row('WHAT IT IS', ctx.description || '—'));
lines.push(row('STACK ', Array.isArray(ctx.stack) ? ctx.stack.join(', ') : (ctx.stack || '—')));
lines.push(row('PATH ', ctx.path));
if (ctx.repo) lines.push(row('REPO ', ctx.repo));
lines.push(row('GOAL ', ctx.goal || '—'));
lines.push(`├${'─'.repeat(W)}┤`);
lines.push(`│ WHERE I LEFT OFF${' '.repeat(W - 18)}│`);
const leftOffLines = (latest.leftOff || '—').split('\n').filter(Boolean);
leftOffLines.forEach(l => lines.push(`│ • ${pad(l, W - 7)}│`));
lines.push(`├${'─'.repeat(W)}┤`);
lines.push(`│ NEXT STEPS${' '.repeat(W - 12)}│`);
const steps = latest.nextSteps || [];
if (steps.length) {
steps.forEach((s, i) => lines.push(`│ ${i + 1}. ${pad(s, W - 8)}│`));
} else {
lines.push(`│ —${' '.repeat(W - 5)}│`);
}
const blockers = latest.blockers?.length ? latest.blockers.join(', ') : 'None';
lines.push(`│ BLOCKERS → ${pad(blockers, W - 13)}│`);
if (latest.gitActivity) {
lines.push(`│ GIT → ${pad(latest.gitActivity, W - 13)}│`);
}
lines.push(`└${'─'.repeat(W)}┘`);
return lines.join('\n');
}
/** Render compact info block used by /ck:info */
export function renderInfoBlock(ctx) {
const latest = ctx.sessions?.[ctx.sessions.length - 1] || {};
const sep = '─'.repeat(44);
const lines = [
`ck: ${ctx.displayName ?? ctx.name}`,
sep,
];
lines.push(`PATH ${ctx.path}`);
if (ctx.repo) lines.push(`REPO ${ctx.repo}`);
if (latest.id) lines.push(`SESSION ${latest.id.slice(0, 8)}`);
lines.push(`GOAL ${ctx.goal || '—'}`);
lines.push(sep);
lines.push(`WHERE I LEFT OFF`);
(latest.leftOff || '—').split('\n').filter(Boolean).forEach(l => lines.push(` • ${l}`));
lines.push(`NEXT STEPS`);
(latest.nextSteps || []).forEach((s, i) => lines.push(` ${i + 1}. ${s}`));
if (!latest.nextSteps?.length) lines.push(` —`);
lines.push(`BLOCKERS`);
if (latest.blockers?.length) {
latest.blockers.forEach(b => lines.push(` • ${b}`));
} else {
lines.push(` • None`);
}
return lines.join('\n');
}
/** Render ASCII list table used by /ck:list */
export function renderListTable(entries, cwd, _todayStr) {
// entries: [{name, contextDir, path, context, lastUpdated}]
// Sorted alphabetically by contextDir before calling
const rows = entries.map((e, i) => {
const isHere = e.path === cwd;
const latest = e.context?.sessions?.[e.context.sessions.length - 1] || {};
const when = daysAgoLabel(latest.date);
const icon = stalenessIcon(latest.date);
const statusLabel = icon === '●' ? '● Active' : icon === '◐' ? '◐ Warm' : '○ Stale';
const sessId = latest.id ? latest.id.slice(0, 8) : '—';
const summary = (latest.summary || '—').slice(0, 34);
const displayName = ((e.context?.displayName ?? e.name) + (isHere ? ' <-' : '')).slice(0, 18);
return {
num: String(i + 1),
name: displayName,
status: statusLabel,
when: when.slice(0, 10),
sessId,
summary,
};
});
const cols = {
num: Math.max(1, ...rows.map(r => r.num.length)),
name: Math.max(7, ...rows.map(r => r.name.length)),
status: Math.max(6, ...rows.map(r => r.status.length)),
when: Math.max(9, ...rows.map(r => r.when.length)),
sessId: Math.max(7, ...rows.map(r => r.sessId.length)),
summary: Math.max(12, ...rows.map(r => r.summary.length)),
};
const hr = `+${'-'.repeat(cols.num + 2)}+${'-'.repeat(cols.name + 2)}+${'-'.repeat(cols.status + 2)}+${'-'.repeat(cols.when + 2)}+${'-'.repeat(cols.sessId + 2)}+${'-'.repeat(cols.summary + 2)}+`;
const cell = (val, width) => ` ${val.padEnd(width)} `;
const headerRow = `|${cell('#', cols.num)}|${cell('Project', cols.name)}|${cell('Status', cols.status)}|${cell('Last Seen', cols.when)}|${cell('Session', cols.sessId)}|${cell('Last Summary', cols.summary)}|`;
const dataRows = rows.map(r =>
`|${cell(r.num, cols.num)}|${cell(r.name, cols.name)}|${cell(r.status, cols.status)}|${cell(r.when, cols.when)}|${cell(r.sessId, cols.sessId)}|${cell(r.summary, cols.summary)}|`
);
return [hr, headerRow, hr, ...dataRows, hr].join('\n');
}
#!/usr/bin/env node
/**
* ck — Context Keeper v2
* session-start.mjs — inject compact project context on session start.
*
* Injects ~100 tokens (not ~2,500 like v1).
* SKILL.md is injected separately (still small at ~50 lines).
*
* Features:
* - Compact 5-line summary for registered projects
* - Unsaved session detection → "Last session wasn't saved. Run /ck:save."
* - Git activity since last session
* - Goal mismatch detection vs CLAUDE.md
* - Mini portfolio for unregistered directories
*/
import { readFileSync, writeFileSync, existsSync } from 'fs';
import { resolve } from 'path';
import { homedir } from 'os';
import { spawnSync } from 'child_process';
const CK_HOME = resolve(homedir(), '.claude', 'ck');
const PROJECTS_FILE = resolve(CK_HOME, 'projects.json');
const CURRENT_SESSION = resolve(CK_HOME, 'current-session.json');
const SKILL_FILE = resolve(homedir(), '.claude', 'skills', 'ck', 'SKILL.md');
// ─── Helpers ──────────────────────────────────────────────────────────────────
function readJson(p) {
try { return JSON.parse(readFileSync(p, 'utf8')); } catch { return null; }
}
function daysAgo(dateStr) {
if (!dateStr) return 'unknown';
const diff = Math.floor((Date.now() - new Date(dateStr)) / 86_400_000);
if (diff === 0) return 'today';
if (diff === 1) return '1 day ago';
return `${diff} days ago`;
}
function stalenessIcon(dateStr) {
if (!dateStr) return '○';
const diff = Math.floor((Date.now() - new Date(dateStr)) / 86_400_000);
return diff < 1 ? '●' : diff <= 5 ? '◐' : '○';
}
function gitLogSince(projectPath, sinceDate) {
if (!sinceDate || !existsSync(resolve(projectPath, '.git'))) return null;
try {
const result = spawnSync(
'git',
['-C', projectPath, 'log', '--oneline', `--since=${sinceDate}`],
{ timeout: 3000, stdio: 'pipe', encoding: 'utf8' },
);
if (result.status !== 0) return null;
const output = result.stdout.trim();
const commits = output.split('\n').filter(Boolean).length;
return commits > 0 ? `${commits} commit${commits !== 1 ? 's' : ''} since last session` : null;
} catch { return null; }
}
function extractClaudeMdGoal(projectPath) {
const p = resolve(projectPath, 'CLAUDE.md');
if (!existsSync(p)) return null;
try {
const md = readFileSync(p, 'utf8');
const m = md.match(/## Current Goal\n([\s\S]*?)(?=\n## |$)/);
return m ? m[1].trim().split('\n')[0].trim() : null;
} catch { return null; }
}
// ─── Session ID from stdin ────────────────────────────────────────────────────
function readSessionId() {
try {
const raw = readFileSync(0, 'utf8');
return JSON.parse(raw).session_id || null;
} catch { return null; }
}
// ─── Main ─────────────────────────────────────────────────────────────────────
function main() {
const cwd = process.env.PWD || process.cwd();
const sessionId = readSessionId();
// Load skill (always inject — now only ~50 lines)
const skill = existsSync(SKILL_FILE) ? readFileSync(SKILL_FILE, 'utf8') : '';
const projects = readJson(PROJECTS_FILE) || {};
const entry = projects[cwd];
// Read previous session BEFORE overwriting current-session.json
const prevSession = readJson(CURRENT_SESSION);
// Write current-session.json
try {
writeFileSync(CURRENT_SESSION, JSON.stringify({
sessionId,
projectPath: cwd,
projectName: entry?.name || null,
startedAt: new Date().toISOString(),
}, null, 2), 'utf8');
} catch { /* non-fatal */ }
const parts = [];
if (skill) parts.push(skill);
// ── REGISTERED PROJECT ────────────────────────────────────────────────────
if (entry?.contextDir) {
const contextFile = resolve(CK_HOME, 'contexts', entry.contextDir, 'context.json');
const context = readJson(contextFile);
if (context) {
const latest = context.sessions?.[context.sessions.length - 1] || {};
const sessionDate = latest.date || context.createdAt;
const sessionCount = context.sessions?.length || 0;
const displayName = context.displayName ?? context.name;
// ── Compact summary block (~100 tokens) ──────────────────────────────
const summaryLines = [
`ck: ${displayName} | ${daysAgo(sessionDate)} | ${sessionCount} session${sessionCount !== 1 ? 's' : ''}`,
`Goal: ${context.goal || '—'}`,
latest.leftOff ? `Left off: ${latest.leftOff.split('\n')[0]}` : null,
latest.nextSteps?.length ? `Next: ${latest.nextSteps.slice(0, 2).join(' · ')}` : null,
].filter(Boolean);
// ── Unsaved session detection ─────────────────────────────────────────
if (prevSession?.sessionId && prevSession.sessionId !== sessionId) {
// Check if previous session ID exists in sessions array
const alreadySaved = context.sessions?.some(s => s.id === prevSession.sessionId);
if (!alreadySaved) {
summaryLines.push(`WARNING Last session wasn't saved — run /ck:save to capture it`);
}
}
// ── Git activity ──────────────────────────────────────────────────────
const gitLine = gitLogSince(cwd, sessionDate);
if (gitLine) summaryLines.push(`Git: ${gitLine}`);
// ── Goal mismatch detection ───────────────────────────────────────────
const claudeMdGoal = extractClaudeMdGoal(cwd);
if (claudeMdGoal && context.goal &&
claudeMdGoal.toLowerCase().trim() !== context.goal.toLowerCase().trim()) {
summaryLines.push(`WARNING Goal mismatch — ck: "${context.goal.slice(0, 40)}" · CLAUDE.md: "${claudeMdGoal.slice(0, 40)}"`);
summaryLines.push(` Run /ck:save with updated goal to sync`);
}
parts.push([
`---`,
`## ck: ${displayName}`,
``,
summaryLines.join('\n'),
].join('\n'));
// Instruct Claude to display compact briefing at session start
parts.push([
`---`,
`## ck: SESSION START`,
``,
`IMPORTANT: Display the following as your FIRST message, verbatim:`,
``,
'```',
summaryLines.join('\n'),
'```',
``,
`After the block, add one line: "Ready — what are we working on?"`,
`If you see WARNING lines above, mention them briefly after the block.`,
].join('\n'));
return parts;
}
}
// ── NOT IN A REGISTERED PROJECT ────────────────────────────────────────────
const entries = Object.entries(projects);
if (entries.length === 0) return parts;
// Load and sort by most recent
const recent = entries
.map(([path, info]) => {
const ctx = readJson(resolve(CK_HOME, 'contexts', info.contextDir, 'context.json'));
const latest = ctx?.sessions?.[ctx.sessions.length - 1] || {};
return { name: info.name, path, lastDate: latest.date || '', summary: latest.summary || '—', ctx };
})
.sort((a, b) => (b.lastDate > a.lastDate ? 1 : -1))
.slice(0, 3);
const miniRows = recent.map(p => {
const icon = stalenessIcon(p.lastDate);
const when = daysAgo(p.lastDate);
const name = p.name.padEnd(16).slice(0, 16);
const whenStr = when.padEnd(12).slice(0, 12);
const summary = p.summary.slice(0, 32);
return ` ${name} ${icon} ${whenStr} ${summary}`;
});
const miniStatus = [
`ck — recent projects:`,
` ${'PROJECT'.padEnd(16)} S ${'LAST SEEN'.padEnd(12)} LAST SESSION`,
` ${'─'.repeat(68)}`,
...miniRows,
``,
`Run /ck:list · /ck:resume <name> · /ck:init to register this folder`,
].join('\n');
parts.push([
`---`,
`## ck: SESSION START`,
``,
`IMPORTANT: Display the following as your FIRST message, verbatim:`,
``,
'```',
miniStatus,
'```',
].join('\n'));
return parts;
}
const parts = main();
if (parts.length > 0) {
console.log(JSON.stringify({ additionalContext: parts.join('\n\n---\n\n') }));
}
Related skills
How it compares
Use everything-claude-code-conventions for cross-repo Claude Code rule enforcement; use project-specific memory skills when only one repo needs persistent context loading.
FAQ
What does everything-claude-code-conventions standardize?
everything-claude-code-conventions standardizes Claude Code prompts, file layouts, workflow hooks, and command patterns so agent sessions produce consistent behavior across contributors and repositories in ECC-style setups.
When should teams adopt everything-claude-code-conventions?
everything-claude-code-conventions fits onboarding new engineers to a shared Claude Code stack, fixing convention drift between repos, or codifying rules that must apply on every agent invoke.