> For the complete documentation index, see [llms.txt](https://docs.tabnine.com/main/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.tabnine.com/main/getting-started/tabnine-cli/features/hooks/examples-and-best-practices.md).

# 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 %}


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.tabnine.com/main/getting-started/tabnine-cli/features/hooks/examples-and-best-practices.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
