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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ Fires when Claude needs user attention.

| Hook | Matcher | Description |
| ------------------------------------------------------------------- | -------------------------------- | ------------------------------------------ |
| [notify-permission](hook-scripts/notification/notify-permission.js) | `permission_prompt\|idle_prompt` | Sends Slack alerts when Claude needs input |
| [notify-permission](hook-scripts/notification/notify-permission.js) | `permission_prompt\|idle_prompt\|elicitation_dialog` | Sends Slack alerts when Claude needs input |

### Utils

Expand Down Expand Up @@ -217,7 +217,7 @@ Everything defaults to **deny** — ask mode is strictly opt-in. A common setup:

## 🧪 Testing

Requires **Node ≥ 18** (no dependencies to install). All hooks include comprehensive tests, run in CI on Node 18, 20, and 22:
Requires **Node ≥ 18** (no npm dependencies). The `format-code` tests exercise the real formatters, so have `prettier`, `ruff`, and `uv` on your PATH — CI installs them — or expect those few tests to fail. All hooks include comprehensive tests, run in CI on Node 18, 20, and 22:

```bash
# Run all tests
Expand Down
1 change: 1 addition & 0 deletions hook-scripts/post-tool-use/format-code.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ async function main() {
log({ level: 'ERROR', error: e.message });
return console.log('{}');
}
if (data === null || typeof data !== 'object') data = {};

const { tool_name, tool_input, session_id, cwd } = data;

Expand Down
8 changes: 4 additions & 4 deletions hook-scripts/tests/meta/test-discovery.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ const HOOK_SHAPE = /^hook-scripts\/tests\/[^/]+\/[^/]+\.test\.js$/;
const PLUGIN_SHAPE = /^plugins\/[^/]+\/tests\/[^/]+\.test\.js$/;

test('every *.test.js is reachable by the npm test glob', () => {
const all = [
...walk(path.join(ROOT, 'hook-scripts', 'tests')),
...walk(path.join(ROOT, 'plugins')),
];
// Walk the whole repo, not just the expected roots — a test file dropped
// anywhere else (hook-scripts/pre-tool-use/, site/, repo root, …) must
// fail this guard too, since the npm glob can't see it there either.
const all = walk(ROOT);

assert.ok(all.length > 0, 'expected to discover at least one test file');

Expand Down
8 changes: 6 additions & 2 deletions hook-scripts/tests/post-tool-use/auto-stage.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,17 @@ const { isInGitRepo, stageFile } = require('../../post-tool-use/auto-stage.js');

const SCRIPT_PATH = path.join(__dirname, '../../post-tool-use/auto-stage.js');

// Hermetic HOME: the hook logs to ~/.claude/hooks-logs — keep test noise
// out of the real home directory. realpathSync: /var vs /private/var on macOS.
const TEST_HOME = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'hook-test-home-')));

// ─────────────────────────────────────────────────────────────────────────────
// Test helpers
// ─────────────────────────────────────────────────────────────────────────────

function runHook(toolName, toolInput, cwd = '/tmp') {
return new Promise((resolve, reject) => {
const child = spawn('node', [SCRIPT_PATH]);
const child = spawn('node', [SCRIPT_PATH], { env: { ...process.env, HOME: TEST_HOME } });
let stdout = '', stderr = '';

child.stdout.on('data', d => stdout += d);
Expand Down Expand Up @@ -154,7 +158,7 @@ describe('Integration: stdin/stdout hook flow', () => {
});

it('handles malformed JSON', async () => {
const child = spawn('node', [SCRIPT_PATH]);
const child = spawn('node', [SCRIPT_PATH], { env: { ...process.env, HOME: TEST_HOME } });
let stdout = '';
const result = await new Promise(resolve => {
child.stdout.on('data', d => stdout += d);
Expand Down
8 changes: 6 additions & 2 deletions hook-scripts/tests/post-tool-use/format-code.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ const { getFormatter, formatFile, log, FORMATTERS } = require('../../post-tool-u

const SCRIPT_PATH = path.join(__dirname, '../../post-tool-use/format-code.js');

// Hermetic HOME: the hook logs to ~/.claude/hooks-logs — keep test noise
// out of the real home directory. realpathSync: /var vs /private/var on macOS.
const TEST_HOME = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'hook-test-home-')));

// ----------------------------------------------------------------------------
// Test helpers
// ----------------------------------------------------------------------------
Expand Down Expand Up @@ -44,7 +48,7 @@ function readContent(filePath) {

function runHook(hookData) {
return new Promise((resolve, reject) => {
const child = spawn('node', [SCRIPT_PATH]);
const child = spawn('node', [SCRIPT_PATH], { env: { ...process.env, HOME: TEST_HOME } });
let stdout = '';

child.stdout.on('data', (data) => { stdout += data; });
Expand Down Expand Up @@ -252,7 +256,7 @@ describe('Integration: stdin/stdout hook flow', () => {
});

it('returns {} for malformed JSON gracefully', async () => {
const child = spawn('node', [SCRIPT_PATH]);
const child = spawn('node', [SCRIPT_PATH], { env: { ...process.env, HOME: TEST_HOME } });
let stdout = '';

const result = await new Promise((resolve) => {
Expand Down
7 changes: 6 additions & 1 deletion plugins/standup-autopilot/tests/standup-autopilot.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -688,7 +688,12 @@ describe('Integration: spawn with temp HOME', () => {
try {
const repo = path.join(home, 'myrepo');
fs.mkdirSync(repo);
const git = (...args) => require('node:child_process').execFileSync('git', args, { cwd: repo, stdio: 'pipe' });
// GIT_CONFIG_GLOBAL/SYSTEM=/dev/null: a contributor's real gitconfig
// (e.g. commit.gpgsign=true) must not leak into this hermetic repo.
const git = (...args) => require('node:child_process').execFileSync('git', args, {
cwd: repo, stdio: 'pipe',
env: { ...process.env, GIT_CONFIG_GLOBAL: '/dev/null', GIT_CONFIG_SYSTEM: '/dev/null' },
});
git('init', '-q');
git('checkout', '-qb', 'feat/standup-test');
fs.writeFileSync(path.join(repo, 'a.txt'), 'one\n');
Expand Down
Loading