mirror of
https://github.com/Xevion/dotfiles.git
synced 2026-01-31 00:24:06 -06:00
feat: add chezmoi safety hooks to prevent unsafe apply/force commands
This commit is contained in:
@@ -0,0 +1,101 @@
|
|||||||
|
#!/usr/bin/env bun
|
||||||
|
/**
|
||||||
|
* Chezmoi Safety Hook - Prevents unsafe chezmoi commands in Claude Code
|
||||||
|
*
|
||||||
|
* Blocks:
|
||||||
|
* - Commands with --force/-f flags
|
||||||
|
* - Commands without specific file arguments (chezmoi apply with no files)
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface ToolInput {
|
||||||
|
command?: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HookInput {
|
||||||
|
tool_name: string;
|
||||||
|
tool_input: ToolInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readStdin(): Promise<string> {
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
for await (const chunk of Bun.stdin.stream()) {
|
||||||
|
chunks.push(Buffer.from(chunk));
|
||||||
|
}
|
||||||
|
return Buffer.concat(chunks).toString("utf-8");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
let inputData: HookInput;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const input = await readStdin();
|
||||||
|
inputData = JSON.parse(input);
|
||||||
|
} catch {
|
||||||
|
// Invalid JSON, allow command to proceed
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const toolName = inputData.tool_name ?? "";
|
||||||
|
const toolInput = inputData.tool_input ?? {};
|
||||||
|
const command = toolInput.command ?? "";
|
||||||
|
|
||||||
|
// Only validate Bash commands
|
||||||
|
if (toolName !== "Bash") {
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only validate chezmoi commands
|
||||||
|
if (!/\bchezmoi\b/.test(command)) {
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const errors: string[] = [];
|
||||||
|
|
||||||
|
// Check for force flags
|
||||||
|
if (/\b(?:--force|-f)\b/.test(command)) {
|
||||||
|
errors.push(
|
||||||
|
"Force flag detected: chezmoi commands with --force or -f are not allowed"
|
||||||
|
);
|
||||||
|
errors.push(
|
||||||
|
" Reason: Force flag bypasses safety checks and can overwrite changes"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for apply/update without specific files
|
||||||
|
if (/\bchezmoi\s+(?:apply|update)\b/.test(command)) {
|
||||||
|
const match = command.match(/\bchezmoi\s+(?:apply|update)\s+(.*)/);
|
||||||
|
if (match) {
|
||||||
|
let args = match[1].trim();
|
||||||
|
// Remove known flags to see if file arguments remain
|
||||||
|
args = args.replace(/(?:--[\w-]+|-[a-z])\b/g, "").trim();
|
||||||
|
|
||||||
|
if (!args) {
|
||||||
|
errors.push(
|
||||||
|
"No specific files specified: 'chezmoi apply' must target specific files"
|
||||||
|
);
|
||||||
|
errors.push(" Allowed: chezmoi apply ~/.bashrc");
|
||||||
|
errors.push(
|
||||||
|
" Allowed: chezmoi apply --dry-run ~/.config/fish/config.fish"
|
||||||
|
);
|
||||||
|
errors.push(" Blocked: chezmoi apply");
|
||||||
|
errors.push(" Blocked: chezmoi apply --dry-run");
|
||||||
|
errors.push("");
|
||||||
|
errors.push(
|
||||||
|
" Use 'chezmoi diff' to preview changes before asking me to apply"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If errors found, block the command
|
||||||
|
if (errors.length > 0) {
|
||||||
|
process.stderr.write(errors.join("\n") + "\n");
|
||||||
|
process.exit(2); // Exit code 2 blocks the command
|
||||||
|
}
|
||||||
|
|
||||||
|
// No issues, allow command
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
/**
|
||||||
|
* Chezmoi Safety Plugin for OpenCode
|
||||||
|
*
|
||||||
|
* Blocks unsafe chezmoi commands:
|
||||||
|
* - Commands with --force/-f flags
|
||||||
|
* - Commands without specific file arguments (chezmoi apply with no files)
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface PluginInput {
|
||||||
|
project: unknown;
|
||||||
|
client: unknown;
|
||||||
|
$: unknown;
|
||||||
|
directory: string;
|
||||||
|
worktree: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ToolExecuteInput {
|
||||||
|
tool: string;
|
||||||
|
sessionID: string;
|
||||||
|
callID: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ToolExecuteOutput {
|
||||||
|
args: {
|
||||||
|
command?: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ChezmoiSafety = async (_ctx: PluginInput) => {
|
||||||
|
return {
|
||||||
|
"tool.execute.before": async (
|
||||||
|
input: ToolExecuteInput,
|
||||||
|
output: ToolExecuteOutput
|
||||||
|
): Promise<void> => {
|
||||||
|
// Only validate Bash commands
|
||||||
|
if (input.tool.toLowerCase() !== "bash") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const command = output.args.command ?? "";
|
||||||
|
|
||||||
|
// Only validate chezmoi commands
|
||||||
|
if (!/\bchezmoi\b/.test(command)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const errors: string[] = [];
|
||||||
|
|
||||||
|
// Check for force flags
|
||||||
|
if (/\b(?:--force|-f)\b/.test(command)) {
|
||||||
|
errors.push(
|
||||||
|
"Force flag detected: chezmoi commands with --force or -f are not allowed"
|
||||||
|
);
|
||||||
|
errors.push(
|
||||||
|
" Reason: Force flag bypasses safety checks and can overwrite changes"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for apply/update without specific files
|
||||||
|
if (/\bchezmoi\s+(?:apply|update)\b/.test(command)) {
|
||||||
|
const match = command.match(/\bchezmoi\s+(?:apply|update)\s+(.*)/);
|
||||||
|
if (match) {
|
||||||
|
let args = match[1].trim();
|
||||||
|
// Remove known flags to see if file arguments remain
|
||||||
|
args = args.replace(/(?:--[\w-]+|-[a-z])\b/g, "").trim();
|
||||||
|
|
||||||
|
if (!args) {
|
||||||
|
errors.push(
|
||||||
|
"No specific files specified: 'chezmoi apply' must target specific files"
|
||||||
|
);
|
||||||
|
errors.push(" Allowed: chezmoi apply ~/.bashrc");
|
||||||
|
errors.push(
|
||||||
|
" Allowed: chezmoi apply --dry-run ~/.config/fish/config.fish"
|
||||||
|
);
|
||||||
|
errors.push(" Blocked: chezmoi apply");
|
||||||
|
errors.push(" Blocked: chezmoi apply --dry-run");
|
||||||
|
errors.push("");
|
||||||
|
errors.push(
|
||||||
|
" Use 'chezmoi diff' to preview changes before asking me to apply"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If errors found, block the command
|
||||||
|
if (errors.length > 0) {
|
||||||
|
throw new Error(errors.join("\n"));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user