Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/auto-0857fcdcbf3d9bd7.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"claude-auto": minor
---

- Allowed users to author their own validators in project-local `.claude-auto/validators/` while keeping the plugin's bundled validators protected from modification
5 changes: 5 additions & 0 deletions .changeset/auto-5e224abf2b4cfe6a.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"claude-auto": patch
---

- Clarified that plugin-bundled validators and reminders are loaded from the plugin root directory
6 changes: 6 additions & 0 deletions .changeset/auto-75b2174d24fca1c8.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"claude-auto": patch
---

- Users can now freely create, edit, and delete their own validators in .claude-auto/validators/
- Plugin's bundled validators remain protected from modification
5 changes: 5 additions & 0 deletions .changeset/auto-b542adb04892b93f.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"claude-auto": patch
---

- Planned documentation fix for plugin-bundled validator path
5 changes: 5 additions & 0 deletions .changeset/auto-c751f6f39b792521.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"claude-auto": patch
---

- Updated ketchup plan to reflect progress on completed bursts
5 changes: 5 additions & 0 deletions .changeset/auto-c7a8d52cb82473d2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"claude-auto": patch
---

- Updated internal ketchup plan to mark burst 3 as complete
9 changes: 9 additions & 0 deletions .changeset/auto-f5fb70c4c93d181a.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"claude-auto": minor
---

- Migrated to plugin-only mode with native Claude Code plugin support via the BeOnAuto/auto-plugins marketplace, removing the legacy npx CLI installation
- Added a new config skill with runtime overrides for validators and reminders, plus first-setup guidance on initial plugin use
- Added support for user-defined custom validators and reminders with documentation in the README
- Fixed commit validation to respect the validateCommit.mode off setting, and resolved plugin path handling when only CLAUDE_PLUGIN_ROOT is set
- Updated all documentation and install instructions for plugin-only mode
4 changes: 3 additions & 1 deletion dist/bundle/scripts/auto-continue.js
Original file line number Diff line number Diff line change
Expand Up @@ -261,12 +261,14 @@ async function resolvePathsFromEnv(explicitPluginRoot) {
const projectRoot = process.cwd();
const claudeDir = path4.join(projectRoot, ".claude");
const autoDir = path4.join(projectRoot, AUTO_DIR);
const pluginValidatorsDir = path4.join(pluginRoot, "validators");
return {
projectRoot,
claudeDir,
autoDir,
remindersDirs: [path4.join(pluginRoot, "reminders"), path4.join(autoDir, "reminders")],
validatorsDirs: [path4.join(pluginRoot, "validators"), path4.join(autoDir, "validators")]
validatorsDirs: [pluginValidatorsDir, path4.join(autoDir, "validators")],
protectedValidatorsDirs: [pluginValidatorsDir]
};
}

Expand Down
4 changes: 3 additions & 1 deletion dist/bundle/scripts/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3815,12 +3815,14 @@ async function resolvePathsFromEnv(explicitPluginRoot) {
const projectRoot = process.cwd();
const claudeDir = path3.join(projectRoot, ".claude");
const autoDir = path3.join(projectRoot, AUTO_DIR);
const pluginValidatorsDir = path3.join(pluginRoot, "validators");
return {
projectRoot,
claudeDir,
autoDir,
remindersDirs: [path3.join(pluginRoot, "reminders"), path3.join(autoDir, "reminders")],
validatorsDirs: [path3.join(pluginRoot, "validators"), path3.join(autoDir, "validators")]
validatorsDirs: [pluginValidatorsDir, path3.join(autoDir, "validators")],
protectedValidatorsDirs: [pluginValidatorsDir]
};
}

Expand Down
8 changes: 5 additions & 3 deletions dist/bundle/scripts/pre-tool-use.js
Original file line number Diff line number Diff line change
Expand Up @@ -6828,7 +6828,7 @@ async function handlePreToolUse(paths, sessionId, toolInput, options2 = {}) {
return handleCommitValidation(paths, sessionId, command, options2, gitCwd);
}
if (command) {
const targetedPath = commandTargetsProtectedPath(command, paths.validatorsDirs);
const targetedPath = commandTargetsProtectedPath(command, paths.protectedValidatorsDirs);
if (targetedPath) {
activityLog(paths.autoDir, sessionId, "pre-tool-use", `blocked protected: ${targetedPath}`);
debugLog(paths.autoDir, "pre-tool-use", `${targetedPath} blocked (immutable validator)`);
Expand All @@ -6842,7 +6842,7 @@ async function handlePreToolUse(paths, sessionId, toolInput, options2 = {}) {
}
}
const filePath = toolInput.file_path;
if (filePath && isProtectedPath(filePath, paths.validatorsDirs)) {
if (filePath && isProtectedPath(filePath, paths.protectedValidatorsDirs)) {
activityLog(paths.autoDir, sessionId, "pre-tool-use", `blocked protected: ${filePath}`);
debugLog(paths.autoDir, "pre-tool-use", `${filePath} blocked (immutable validator)`);
return {
Expand Down Expand Up @@ -6952,12 +6952,14 @@ async function resolvePathsFromEnv(explicitPluginRoot) {
const projectRoot = process.cwd();
const claudeDir = path8.join(projectRoot, ".claude");
const autoDir = path8.join(projectRoot, AUTO_DIR);
const pluginValidatorsDir = path8.join(pluginRoot, "validators");
return {
projectRoot,
claudeDir,
autoDir,
remindersDirs: [path8.join(pluginRoot, "reminders"), path8.join(autoDir, "reminders")],
validatorsDirs: [path8.join(pluginRoot, "validators"), path8.join(autoDir, "validators")]
validatorsDirs: [pluginValidatorsDir, path8.join(autoDir, "validators")],
protectedValidatorsDirs: [pluginValidatorsDir]
};
}

Expand Down
4 changes: 3 additions & 1 deletion dist/bundle/scripts/session-start.js
Original file line number Diff line number Diff line change
Expand Up @@ -3863,12 +3863,14 @@ async function resolvePathsFromEnv(explicitPluginRoot) {
const projectRoot = process.cwd();
const claudeDir = path6.join(projectRoot, ".claude");
const autoDir = path6.join(projectRoot, AUTO_DIR);
const pluginValidatorsDir = path6.join(pluginRoot, "validators");
return {
projectRoot,
claudeDir,
autoDir,
remindersDirs: [path6.join(pluginRoot, "reminders"), path6.join(autoDir, "reminders")],
validatorsDirs: [path6.join(pluginRoot, "validators"), path6.join(autoDir, "validators")]
validatorsDirs: [pluginValidatorsDir, path6.join(autoDir, "validators")],
protectedValidatorsDirs: [pluginValidatorsDir]
};
}

Expand Down
4 changes: 3 additions & 1 deletion dist/bundle/scripts/user-prompt-submit.js
Original file line number Diff line number Diff line change
Expand Up @@ -3879,12 +3879,14 @@ async function resolvePathsFromEnv(explicitPluginRoot) {
const projectRoot = process.cwd();
const claudeDir = path6.join(projectRoot, ".claude");
const autoDir = path6.join(projectRoot, AUTO_DIR);
const pluginValidatorsDir = path6.join(pluginRoot, "validators");
return {
projectRoot,
claudeDir,
autoDir,
remindersDirs: [path6.join(pluginRoot, "reminders"), path6.join(autoDir, "reminders")],
validatorsDirs: [path6.join(pluginRoot, "validators"), path6.join(autoDir, "validators")]
validatorsDirs: [pluginValidatorsDir, path6.join(autoDir, "validators")],
protectedValidatorsDirs: [pluginValidatorsDir]
};
}

Expand Down
4 changes: 2 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ Reminders are Markdown files with YAML frontmatter that inject context into Clau

### Location

- Default reminders: `.claude-auto/reminders/` (provided by plugin)
- Default reminders: `$CLAUDE_PLUGIN_ROOT/reminders/` (bundled with the plugin, immutable)
- Custom reminders: `.claude-auto/reminders/` (add your own `.md` files)

### Frontmatter Schema
Expand Down Expand Up @@ -303,7 +303,7 @@ Validators are Markdown files with YAML frontmatter.

### Location

- Default validators: `.claude-auto/validators/` (provided by plugin)
- Default validators: `$CLAUDE_PLUGIN_ROOT/validators/` (bundled with the plugin, immutable)
- Custom validators: `.claude-auto/validators/` (add your own `.md` files)

### Frontmatter Schema
Expand Down
7 changes: 4 additions & 3 deletions ketchup-plan.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# Ketchup Plan: Fix release workflow after marketplace.json removal
# Ketchup Plan: Allow user-defined validators

## TODO

## DONE

- [x] Burst 2: Remove legacy npm publish from release workflow, rewire tag/release/marketplace gates (a21da44)
- [x] Burst 1: Remove `marketplace.json` from version-bump loop in ci.yml (2e153e9)
- [x] Burst 1: resolvePathsFromEnv returns protectedValidatorsDirs field (5e9d98f)
- [x] Burst 2: handlePreToolUse uses protectedValidatorsDirs for immutability checks (4ac31ff)
- [x] Burst 3: Fix configuration.md to show plugin-bundled validators and reminders live at $CLAUDE_PLUGIN_ROOT, not in .claude-auto/ (050bcb9)
1 change: 1 addition & 0 deletions src/config-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ describe('config-manager', () => {
autoDir,
validatorsDirs: [validatorsDir],
remindersDirs: [remindersDir, projectRemindersDir],
protectedValidatorsDirs: [],
};

fs.writeFileSync(
Expand Down
30 changes: 24 additions & 6 deletions src/hooks/pre-tool-use.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ describe('pre-tool-use hook', () => {
autoDir,
remindersDirs: [path.join(autoDir, 'reminders')],
validatorsDirs: [path.join(autoDir, 'validators')],
protectedValidatorsDirs: [],
};
fs.mkdirSync(claudeDir, { recursive: true });
fs.mkdirSync(autoDir, { recursive: true });
Expand Down Expand Up @@ -336,11 +337,13 @@ Validate this commit`,
}
});

it('denies Bash command targeting validator files', async () => {
const validatorPath = path.join(autoDir, 'validators', 'burst-atomicity.md');
it('denies Bash command targeting protected (plugin) validator files', async () => {
const pluginValidatorsDir = '/plugins/claude-auto/validators';
const paths = { ...resolvedPaths, protectedValidatorsDirs: [pluginValidatorsDir] };
const validatorPath = path.join(pluginValidatorsDir, 'burst-atomicity.md');
const toolInput = { command: `rm ${validatorPath}` };

const result = await handlePreToolUse(resolvedPaths, 'session-bash-protect', toolInput);
const result = await handlePreToolUse(paths, 'session-bash-protect', toolInput);

expect(result).toEqual({
hookSpecificOutput: {
Expand All @@ -351,10 +354,12 @@ Validate this commit`,
});
});

it('denies Edit/Write to validator files', async () => {
const toolInput = { file_path: path.join(autoDir, 'validators', 'burst-atomicity.md') };
it('denies Edit/Write to protected (plugin) validator files', async () => {
const pluginValidatorsDir = '/plugins/claude-auto/validators';
const paths = { ...resolvedPaths, protectedValidatorsDirs: [pluginValidatorsDir] };
const toolInput = { file_path: path.join(pluginValidatorsDir, 'burst-atomicity.md') };

const result = await handlePreToolUse(resolvedPaths, 'session-protect', toolInput);
const result = await handlePreToolUse(paths, 'session-protect', toolInput);

expect(result).toEqual({
hookSpecificOutput: {
Expand All @@ -365,6 +370,19 @@ Validate this commit`,
});
});

it('allows Edit/Write to project-local validator files (not in protectedValidatorsDirs)', async () => {
const toolInput = { file_path: path.join(autoDir, 'validators', 'my-custom.md') };

const result = await handlePreToolUse(resolvedPaths, 'session-user-validator', toolInput);

expect(result).toEqual({
hookSpecificOutput: {
hookEventName: 'PreToolUse',
permissionDecision: 'allow',
},
});
});

describe('isProtectedPath', () => {
it('returns true for file inside a validatorsDirs path', () => {
const validatorsDirs = ['/plugin/validators', '/project/.claude-auto/validators'];
Expand Down
4 changes: 2 additions & 2 deletions src/hooks/pre-tool-use.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export async function handlePreToolUse(
}

if (command) {
const targetedPath = commandTargetsProtectedPath(command, paths.validatorsDirs);
const targetedPath = commandTargetsProtectedPath(command, paths.protectedValidatorsDirs);
if (targetedPath) {
activityLog(paths.autoDir, sessionId, 'pre-tool-use', `blocked protected: ${targetedPath}`);
debugLog(paths.autoDir, 'pre-tool-use', `${targetedPath} blocked (immutable validator)`);
Expand All @@ -87,7 +87,7 @@ export async function handlePreToolUse(

const filePath = toolInput.file_path as string;

if (filePath && isProtectedPath(filePath, paths.validatorsDirs)) {
if (filePath && isProtectedPath(filePath, paths.protectedValidatorsDirs)) {
activityLog(paths.autoDir, sessionId, 'pre-tool-use', `blocked protected: ${filePath}`);
debugLog(paths.autoDir, 'pre-tool-use', `${filePath} blocked (immutable validator)`);
return {
Expand Down
1 change: 1 addition & 0 deletions src/hooks/session-start.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ describe('session-start hook', () => {
autoDir,
remindersDirs: [path.join(autoDir, 'reminders')],
validatorsDirs: [path.join(autoDir, 'validators')],
protectedValidatorsDirs: [],
};
fs.mkdirSync(claudeDir, { recursive: true });
fs.mkdirSync(autoDir, { recursive: true });
Expand Down
1 change: 1 addition & 0 deletions src/hooks/user-prompt-submit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ describe('user-prompt-submit hook', () => {
autoDir,
remindersDirs: [path.join(autoDir, 'reminders')],
validatorsDirs: [path.join(autoDir, 'validators')],
protectedValidatorsDirs: [],
};
fs.mkdirSync(claudeDir, { recursive: true });
fs.mkdirSync(autoDir, { recursive: true });
Expand Down
18 changes: 18 additions & 0 deletions src/path-resolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ describe('resolvePathsFromEnv', () => {
autoDir: path.join(process.cwd(), '.claude-auto'),
validatorsDirs: ['/plugins/claude-auto/validators', path.join(process.cwd(), '.claude-auto', 'validators')],
remindersDirs: ['/plugins/claude-auto/reminders', path.join(process.cwd(), '.claude-auto', 'reminders')],
protectedValidatorsDirs: ['/plugins/claude-auto/validators'],
});
});

Expand All @@ -50,6 +51,7 @@ describe('resolvePathsFromEnv', () => {
autoDir: path.join(process.cwd(), '.claude-auto'),
validatorsDirs: ['/explicit/plugin-root/validators', path.join(process.cwd(), '.claude-auto', 'validators')],
remindersDirs: ['/explicit/plugin-root/reminders', path.join(process.cwd(), '.claude-auto', 'reminders')],
protectedValidatorsDirs: ['/explicit/plugin-root/validators'],
});
});

Expand All @@ -65,6 +67,7 @@ describe('resolvePathsFromEnv', () => {
autoDir: path.join(process.cwd(), '.claude-auto'),
validatorsDirs: ['/plugins/claude-auto/validators', path.join(process.cwd(), '.claude-auto', 'validators')],
remindersDirs: ['/plugins/claude-auto/reminders', path.join(process.cwd(), '.claude-auto', 'reminders')],
protectedValidatorsDirs: ['/plugins/claude-auto/validators'],
});
});

Expand All @@ -79,4 +82,19 @@ describe('resolvePathsFromEnv', () => {
expect(result.validatorsDirs[0]).toBe('/plugins/claude-auto/validators');
expect(result.remindersDirs[0]).toBe('/plugins/claude-auto/reminders');
});

it('exposes protectedValidatorsDirs containing only the plugin validators dir', async () => {
vi.stubEnv('CLAUDE_PLUGIN_ROOT', '/plugins/claude-auto');

const result = await resolvePathsFromEnv();

expect(result).toEqual({
projectRoot: process.cwd(),
claudeDir: path.join(process.cwd(), '.claude'),
autoDir: path.join(process.cwd(), '.claude-auto'),
validatorsDirs: ['/plugins/claude-auto/validators', path.join(process.cwd(), '.claude-auto', 'validators')],
remindersDirs: ['/plugins/claude-auto/reminders', path.join(process.cwd(), '.claude-auto', 'reminders')],
protectedValidatorsDirs: ['/plugins/claude-auto/validators'],
});
});
});
6 changes: 5 additions & 1 deletion src/path-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface ResolvedPaths {
autoDir: string;
remindersDirs: string[];
validatorsDirs: string[];
protectedValidatorsDirs: string[];
}

export async function resolvePathsFromEnv(explicitPluginRoot?: string): Promise<ResolvedPaths> {
Expand All @@ -21,11 +22,14 @@ export async function resolvePathsFromEnv(explicitPluginRoot?: string): Promise<
const claudeDir = path.join(projectRoot, '.claude');
const autoDir = path.join(projectRoot, AUTO_DIR);

const pluginValidatorsDir = path.join(pluginRoot, 'validators');

return {
projectRoot,
claudeDir,
autoDir,
remindersDirs: [path.join(pluginRoot, 'reminders'), path.join(autoDir, 'reminders')],
validatorsDirs: [path.join(pluginRoot, 'validators'), path.join(autoDir, 'validators')],
validatorsDirs: [pluginValidatorsDir, path.join(autoDir, 'validators')],
protectedValidatorsDirs: [pluginValidatorsDir],
};
}
6 changes: 6 additions & 0 deletions src/plugin-debug.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ describe('logPluginDiagnostics', () => {
autoDir: nonExistentDir,
validatorsDirs: ['/plugins/claude-auto/validators'],
remindersDirs: ['/plugins/claude-auto/reminders'],
protectedValidatorsDirs: ['/plugins/claude-auto/validators'],
});

expect(fs.existsSync(nonExistentDir)).toBe(false);
Expand All @@ -44,6 +45,7 @@ describe('logPluginDiagnostics', () => {
autoDir: tempDir,
validatorsDirs: ['/plugins/claude-auto/validators'],
remindersDirs: ['/plugins/claude-auto/reminders'],
protectedValidatorsDirs: ['/plugins/claude-auto/validators'],
});

const logFile = path.join(tempDir, 'logs', 'plugin-debug.log');
Expand All @@ -64,6 +66,7 @@ describe('logPluginDiagnostics', () => {
autoDir: tempDir,
validatorsDirs: ['/project/.claude-auto/validators'],
remindersDirs: ['/project/.claude-auto/reminders'],
protectedValidatorsDirs: [],
});

const logFile = path.join(tempDir, 'logs', 'plugin-debug.log');
Expand All @@ -83,6 +86,7 @@ describe('logPluginDiagnostics', () => {
autoDir: tempDir,
validatorsDirs: ['/plugins/claude-auto/validators'],
remindersDirs: ['/plugins/claude-auto/reminders'],
protectedValidatorsDirs: ['/plugins/claude-auto/validators'],
});

expect(spy).toHaveBeenCalledOnce();
Expand All @@ -101,6 +105,7 @@ describe('logPluginDiagnostics', () => {
autoDir: tempDir,
validatorsDirs: ['/plugins/claude-auto/validators'],
remindersDirs: ['/plugins/claude-auto/reminders'],
protectedValidatorsDirs: ['/plugins/claude-auto/validators'],
});

expect(spy).not.toHaveBeenCalled();
Expand All @@ -119,6 +124,7 @@ describe('logPluginDiagnostics', () => {
autoDir: tempDir,
validatorsDirs: ['/project/.claude-auto/validators'],
remindersDirs: ['/project/.claude-auto/reminders'],
protectedValidatorsDirs: [],
});

expect(spy).not.toHaveBeenCalled();
Expand Down
Loading