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
22 changes: 22 additions & 0 deletions examples/team-config/.claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@
"ANTHROPIC_BASE_URL": "http://127.0.0.1:4000"
},
"hooks": {
"PreToolUse": [
{
"matcher": "mcp__.*",
"hooks": [
{
"type": "command",
"command": "hush redact-hook",
"timeout": 10
}
]
}
],
"PostToolUse": [
{
"matcher": "Bash|Read|Grep|WebFetch",
Expand All @@ -13,6 +25,16 @@
"timeout": 10
}
]
},
{
"matcher": "mcp__.*",
"hooks": [
{
"type": "command",
"command": "hush redact-hook",
"timeout": 10
}
]
}
]
}
Expand Down
38 changes: 38 additions & 0 deletions examples/team-config/.gemini/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"hooks": {
"BeforeTool": [
{
"matcher": "mcp__.*",
"hooks": [
{
"type": "command",
"command": "hush redact-hook",
"timeout": 10
}
]
}
],
"AfterTool": [
{
"matcher": "run_shell_command|read_file|read_many_files|search_file_content|web_fetch",
"hooks": [
{
"type": "command",
"command": "hush redact-hook",
"timeout": 10
}
]
},
{
"matcher": "mcp__.*",
"hooks": [
{
"type": "command",
"command": "hush redact-hook",
"timeout": 10
}
]
}
]
}
}
15 changes: 9 additions & 6 deletions examples/team-config/.opencode/plugins/hush.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
/**
* Hush PII Guard — OpenCode Plugin (drop-in copy)
*
* Blocks reads of sensitive files (.env, *.pem, credentials.*, etc.)
* before the tool executes — the AI model never sees the content.
* This drop-in copy provides file-blocking only (sensitive file reads).
* For full bidirectional PII redaction (tool args + tool results),
* install from npm instead:
*
* Usage: copy this file to `.opencode/plugins/hush.ts` in your project
* and add to `opencode.json`:
* { "plugin": [".opencode/plugins/hush.ts"] }
* npm install @aictrl/hush
*
* Or install from npm:
* Then in your plugin entry point:
* import { HushPlugin } from '@aictrl/hush/opencode-plugin'
*
* Usage (drop-in): copy this file to `.opencode/plugins/hush.ts` in your
* project and add to `opencode.json`:
* { "plugin": [".opencode/plugins/hush.ts"] }
*/

const SENSITIVE_GLOBS = [
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@aictrl/hush",
"version": "0.1.7",
"version": "0.1.8",
"description": "Hush: A Semantic Security Gateway for AI Agents. Redacts PII from prompts and tool outputs locally before they hit the cloud.",
"type": "module",
"main": "dist/index.js",
Expand Down
141 changes: 110 additions & 31 deletions src/commands/init.ts
Original file line number Diff line number Diff line change
@@ -1,87 +1,163 @@
/**
* hush init — Generate Claude Code hook configuration
* hush init — Generate hook configuration for Claude Code or Gemini CLI
*
* Usage:
* hush init --hooks Write to .claude/settings.json
* hush init --hooks --local Write to .claude/settings.local.json
* hush init --hooks --gemini Write to .gemini/settings.json
* hush init --hooks --gemini --local Write to .gemini/settings.local.json
*/

import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs';
import { join } from 'path';

const HOOK_CONFIG = {
const HUSH_HOOK = {
type: 'command' as const,
command: 'hush redact-hook',
timeout: 10,
};

const CLAUDE_HOOK_CONFIG = {
hooks: {
PreToolUse: [
{
matcher: 'mcp__.*',
hooks: [HUSH_HOOK],
},
],
PostToolUse: [
{
matcher: 'Bash|Read|Grep|WebFetch',
hooks: [
{
type: 'command' as const,
command: 'hush redact-hook',
timeout: 10,
},
],
hooks: [HUSH_HOOK],
},
{
matcher: 'mcp__.*',
hooks: [HUSH_HOOK],
},
],
},
};

const GEMINI_HOOK_CONFIG = {
hooks: {
BeforeTool: [
{
matcher: 'mcp__.*',
hooks: [HUSH_HOOK],
},
],
AfterTool: [
{
matcher: 'run_shell_command|read_file|read_many_files|search_file_content|web_fetch',
hooks: [HUSH_HOOK],
},
{
matcher: 'mcp__.*',
hooks: [HUSH_HOOK],
},
],
},
};

interface HookEntry {
matcher: string;
hooks: Array<{ type: string; command: string; timeout?: number }>;
}

interface SettingsJson {
hooks?: {
PostToolUse?: Array<{
matcher: string;
hooks: Array<{ type: string; command: string; timeout?: number }>;
}>;
PreToolUse?: HookEntry[];
PostToolUse?: HookEntry[];
BeforeTool?: HookEntry[];
AfterTool?: HookEntry[];
[key: string]: unknown;
};
[key: string]: unknown;
}

function hasHushHook(settings: SettingsJson): boolean {
const postToolUse = settings.hooks?.PostToolUse;
if (!Array.isArray(postToolUse)) return false;
interface HookConfig {
hooks: Record<string, HookEntry[]>;
}

return postToolUse.some((entry) =>
function hasHushHookInEntries(entries: HookEntry[] | undefined): boolean {
if (!Array.isArray(entries)) return false;
return entries.some((entry) =>
entry.hooks?.some((h) => h.command?.includes('hush redact-hook')),
);
}

function mergeHooks(existing: SettingsJson): SettingsJson {
function hasHushHookClaude(settings: SettingsJson): boolean {
return (
hasHushHookInEntries(settings.hooks?.PreToolUse) &&
hasHushHookInEntries(settings.hooks?.PostToolUse)
);
}

function hasHushHookGemini(settings: SettingsJson): boolean {
return (
hasHushHookInEntries(settings.hooks?.BeforeTool) &&
hasHushHookInEntries(settings.hooks?.AfterTool)
);
}

function mergeHookEntries(
existing: HookEntry[] | undefined,
newEntries: HookEntry[],
): HookEntry[] {
const merged = Array.isArray(existing) ? [...existing] : [];

for (const entry of newEntries) {
const alreadyHas = merged.some(
(e) =>
e.matcher === entry.matcher &&
e.hooks?.some((h) => h.command?.includes('hush redact-hook')),
);
if (!alreadyHas) {
merged.push(entry);
}
}

return merged;
}

function mergeHooks(existing: SettingsJson, hookConfig: HookConfig): SettingsJson {
const merged = { ...existing };

if (!merged.hooks) {
merged.hooks = {};
}

if (!Array.isArray(merged.hooks.PostToolUse)) {
merged.hooks.PostToolUse = [];
for (const [eventName, entries] of Object.entries(hookConfig.hooks)) {
const existingEntries = merged.hooks[eventName] as HookEntry[] | undefined;
merged.hooks[eventName] = mergeHookEntries(existingEntries, entries);
}

merged.hooks = { ...merged.hooks, PostToolUse: [...merged.hooks.PostToolUse, ...HOOK_CONFIG.hooks.PostToolUse] };

return merged;
}

export function run(args: string[]): void {
const hasHooksFlag = args.includes('--hooks');
const isLocal = args.includes('--local');
const isGemini = args.includes('--gemini');

if (!hasHooksFlag) {
process.stderr.write('Usage: hush init --hooks [--local]\n');
process.stderr.write('Usage: hush init --hooks [--local] [--gemini]\n');
process.stderr.write('\n');
process.stderr.write('Options:\n');
process.stderr.write(' --hooks Generate Claude Code PostToolUse hook config\n');
process.stderr.write(' --hooks Generate hook config (PreToolUse + PostToolUse or BeforeTool + AfterTool)\n');
process.stderr.write(' --local Write to settings.local.json instead of settings.json\n');
process.stderr.write(' --gemini Write Gemini CLI hooks instead of Claude Code hooks\n');
process.exit(1);
}

const claudeDir = join(process.cwd(), '.claude');
const dirName = isGemini ? '.gemini' : '.claude';
const configDir = join(process.cwd(), dirName);
const filename = isLocal ? 'settings.local.json' : 'settings.json';
const filePath = join(claudeDir, filename);
const filePath = join(configDir, filename);

// Ensure .claude/ exists
if (!existsSync(claudeDir)) {
mkdirSync(claudeDir, { recursive: true });
// Ensure config dir exists
if (!existsSync(configDir)) {
mkdirSync(configDir, { recursive: true });
}

// Read existing settings or start fresh
Expand All @@ -96,12 +172,15 @@ export function run(args: string[]): void {
}

// Idempotency check
if (hasHushHook(settings)) {
const hookConfig = isGemini ? GEMINI_HOOK_CONFIG : CLAUDE_HOOK_CONFIG;
const hasHook = isGemini ? hasHushHookGemini : hasHushHookClaude;

if (hasHook(settings)) {
process.stdout.write(`hush hooks already configured in ${filePath}\n`);
return;
}

const merged = mergeHooks(settings);
const merged = mergeHooks(settings, hookConfig);
writeFileSync(filePath, JSON.stringify(merged, null, 2) + '\n');
process.stdout.write(`Wrote hush hooks config to ${filePath}\n`);
}
Loading