# Examples & Best Practices

Copy-paste hook recipes, security model, and testing tips for Tabnine CLI.

## Examples

### Blocking dangerous shell commands

**`.tabnine/agent/settings.json`**

```json
{
  "hooks": {
    "BeforeTool": [
      {
        "matcher": "run_shell_command",
        "sequential": true,
        "hooks": [
          {
            "type": "command",
            "command": "node ./hooks/block-dangerous.js",
            "name": "block-dangerous",
            "description": "Block dangerous shell commands"
          }
        ]
      }
    ]
  }
}
```

**`hooks/block-dangerous.js`**

```javascript
let data = '';
process.stdin.on('data', (chunk) => { data += chunk; });
process.stdin.on('end', () => {
  const input = JSON.parse(data);
  const cmd = input.tool_input?.command || '';

  if (cmd.includes('rm -rf') || cmd.includes('sudo')) {
    // Block via JSON output (preferred over exit code 2)
    process.stdout.write(JSON.stringify({
      decision: 'deny',
      reason: 'Blocked dangerous command: ' + cmd
    }));
  } else {
    process.stdout.write(JSON.stringify({ decision: 'allow' }));
  }
});
```

### Restricting file writes to a directory

**`hooks/enforce-directory.js`**

```javascript
let data = '';
process.stdin.on('data', (chunk) => { data += chunk; });
process.stdin.on('end', () => {
  const input = JSON.parse(data);

  if (input.tool_name === 'write_file') {
    const filePath = input.tool_input.file_path;
    if (!filePath.startsWith('/allowed/dir/')) {
      process.stdout.write(JSON.stringify({
        decision: 'deny',
        reason: 'File writes only allowed under /allowed/dir/'
      }));
      return;
    }
  }

  process.stdout.write(JSON.stringify({ decision: 'allow' }));
});
```

### Logging all tool calls

**`.tabnine/agent/settings.json`**

```json
{
  "hooks": {
    "AfterTool": [
      {
        "matcher": ".*",
        "hooks": [
          {
            "type": "command",
            "command": "node ./hooks/log-tools.js",
            "name": "tool-logger"
          }
        ]
      }
    ]
  }
}
```

**`hooks/log-tools.js`**

```javascript
const fs = require('fs');
let data = '';
process.stdin.on('data', (chunk) => { data += chunk; });
process.stdin.on('end', () => {
  const input = JSON.parse(data);
  const line = `[${input.timestamp}] ${input.tool_name}: ${JSON.stringify(input.tool_input)}
`;
  fs.appendFileSync('tool-log.txt', line);
  process.exit(0);
});
```

### Adding context to every prompt

**`.tabnine/agent/settings.json`**

```json
{
  "hooks": {
    "BeforeAgent": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "node ./hooks/add-context.js",
            "name": "project-context",
            "timeout": 5000
          }
        ]
      }
    ]
  }
}
```

**`hooks/add-context.js`**

```javascript
const { execSync } = require('child_process');
let data = '';
process.stdin.on('data', (chunk) => { data += chunk; });
process.stdin.on('end', () => {
  const input = JSON.parse(data);
  const context = `Current time: ${new Date().toISOString()}
`
    + `Git branch: ${execSync('git branch --show-current').toString().trim()}
`;

  process.stdout.write(JSON.stringify({
    hookSpecificOutput: { additionalContext: context }
  }));
});
```

### Auto-running linter after file changes

**`.tabnine/agent/settings.json`**

```json
{
  "hooks": {
    "AfterTool": [
      {
        "matcher": "write_file|replace",
        "sequential": true,
        "hooks": [
          {
            "type": "command",
            "command": "node ./hooks/auto-lint.js",
            "name": "auto-lint",
            "description": "Run linter after file modifications",
            "timeout": 10000
          }
        ]
      }
    ]
  }
}
```

**`hooks/auto-lint.js`**

```javascript
const { execSync } = require('child_process');
let data = '';
process.stdin.on('data', (chunk) => { data += chunk; });
process.stdin.on('end', () => {
  const input = JSON.parse(data);
  const filePath = input.tool_input?.file_path;

  if (filePath && filePath.endsWith('.ts')) {
    try {
      execSync(`npx eslint --fix "${filePath}"`, { stdio: 'pipe' });
      process.stdout.write(JSON.stringify({
        hookSpecificOutput: {
          additionalContext: `Linter ran on ${filePath}`
        }
      }));
    } catch (e) {
      process.stdout.write(JSON.stringify({
        hookSpecificOutput: {
          additionalContext: `Lint errors found in ${filePath}: ${e.stderr?.toString() || ''}`
        }
      }));
    }
  } else {
    process.exit(0);
  }
});
```

## Security and trust

{% hint style="warning" %}
Hooks execute arbitrary code with your user privileges.

**Project-level hooks** (in `.tabnine/agent/settings.json` within a project) are particularly risky when opening untrusted projects. Tabnine CLI **fingerprints** project hooks — if a hook's command changes (e.g., via `git pull`), it is treated as a **new, untrusted hook** and a warning is displayed before execution.

Untrusted folders will not have their project-level hooks loaded. Use the `/permissions` command in the CLI to manage folder trust.
{% endhint %}

## Best practices

{% stepper %}
{% step %}

### Keep hooks fast

Hooks run synchronously and block the agent loop. Aim for sub-second execution. Use the `timeout` field to prevent hangs.
{% endstep %}

{% step %}

### Always output valid JSON to stdout

Any non-JSON output to stdout will cause the hook to be treated as a plain-text system message with "allow" decision.
{% endstep %}

{% step %}

### Use stderr for logging

Never use `console.log()` or `echo` for debug output — use `console.error()` or `>&2` to write to stderr instead.
{% endstep %}

{% step %}

### Prefer exit code 0 with `decision: "deny"` over exit code 2

For intentional blocks, exit code 0 with JSON output gives you full control over the feedback message and behavior.
{% endstep %}

{% step %}

### Use descriptive `name` fields

The name appears in logs and can be used in `hooksConfig.disabled` to selectively turn off hooks without removing them.
{% endstep %}

{% step %}

### Test hooks independently

Before adding them to settings:

```bash
echo '{"hook_event_name":"BeforeTool","tool_name":"write_file","tool_input":{"file_path":"test.txt","content":"hello"},"cwd":"/tmp","session_id":"test","timestamp":"2025-01-01T00:00:00.000Z"}' | node ./hooks/my-hook.js
```

{% endstep %}

{% step %}

### Set the `sequential` flag

Use it when hook execution order matters (e.g., a validation hook must run before a logging hook).
{% endstep %}

{% step %}

### Read stdin via the stream API

Use `process.stdin.on('data', ...)` rather than `fs.readFileSync('/dev/stdin')` for cross-platform compatibility.
{% endstep %}
{% endstepper %}
