npm package: @ottoai/uignore
Description: Unified ignore rules for AI coding tools — enforce .uignore exclusions via hooks for Claude Code and Gemini CLI
Author: Anurag Patel
Latest version: 1.2.0

# Ignoring files

This document provides an overview of the uignore (`.uignore`) feature for AI coding tools.

uignore gives you a single `.uignore` file that works across all supported AI coding tools,
similar to `.gitignore` (used by Git). Adding paths to your `.uignore` file will exclude them
from all supported AI tools, although they will still be visible to other services (such as Git).

## Supported tools

| Tool | Hook | Config file |
|---|---|---|
| [Claude Code](https://claude.ai/claude-code) | `PreToolUse` | `.claude/settings.json` |
| [Gemini CLI](https://github.com/google-gemini/gemini-cli) | `BeforeTool` | `.gemini/settings.json` |
| [Cursor](https://www.cursor.com) | `beforeReadFile` | `.cursor/hooks.json` |
| [Windsurf](https://windsurf.com) | `pre_read_code` | `.windsurf/hooks.json` |
| Codex CLI | — | coming soon |

## How it works

When you add a path to your `.uignore` file, all supported AI tools will exclude matching files
and directories from their operations. For example, when Claude Code tries to read a file, any
paths in your `.uignore` will be automatically blocked before the tool call executes.

uignore registers a native hook with each AI tool. Before every file operation, the hook checks
whether the target path matches any rule in your `.uignore`. If it does, the hook exits with
code `2` — a hard block — and the tool is prevented from accessing the file.

uignore also supports hierarchical `.uignore` files. Starting from the target file's directory,
it searches upward through parent directories to find all `.uignore` files up to the project
root. Patterns from all discovered files are combined, with files closer to the root applied
first.

A global `~/.uignore` file is also supported and loaded before project-level files, giving
you personal rules that apply across every project.

Since hooks are registered in config files that can be committed to git, the whole team gets
the same rules automatically.

## Installation

```bash
npm install -g @ottoai/uignore
```

## Quick start (no global install)

You can use uignore without a global install via `npx`:

```bash
npx @ottoai/uignore init
npx @ottoai/uignore install --yes
```

Note: because `npx` resolves a new path each time, the hook command written
into your settings files will point to the npx cache. Run `uignore install`
once with a global install (`npm install -g @ottoai/uignore`) for a stable
path, or use `uignore update` after each `npm update -g`.

## How to use `.uignore`

To enable `.uignore`:

1. Run `uignore init` in your project root to create a `.uignore` file.
2. Run `uignore install` to register the hook with all supported AI tools.
3. Commit both files to git so your whole team benefits.

```bash
uignore init      # creates a .uignore template
uignore install   # registers hooks for all supported tools
```

```bash
git add .uignore .claude/settings.json .gemini/settings.json .cursor/hooks.json .windsurf/hooks.json
git commit -m "chore: add uignore"
```

To add a file or directory to `.uignore`:

1. Open your `.uignore` file.
2. Add the path or file you want to ignore, for example: `secrets/` or `apikeys.txt`.

You can update your `.uignore` file at any time. Changes take effect immediately — no restart
required.

To remove a path from `.uignore`, delete the relevant line.

## `.uignore` syntax

For the most part, `.uignore` follows the conventions of `.gitignore` files:

- Blank lines and lines starting with `#` are ignored.
- Standard glob patterns are supported (such as `*`, `?`, and `[]`).
- Putting a `/` at the end will only match directories.
- Putting a `/` at the beginning anchors the path relative to the `.uignore` file.
- `!` negates a pattern.

### `.uignore` examples

You can use `.uignore` to ignore directories and files:

```gitignore
# Exclude your secrets/ directory and all subdirectories
secrets/

# Exclude your apikeys.txt file
apikeys.txt
```

You can use wildcards in your `.uignore` file with `*`:

```gitignore
# Exclude all .pem files
*.pem

# Exclude all .env files
.env
.env.*
```

You can exclude files and directories from exclusion with `!`:

```gitignore
# Exclude all .md files except README.md
*.md
!README.md
```

You can use hierarchical `.uignore` files for subtree-specific rules:

```
project/
  .uignore          # global rules — apply everywhere
  src/
    .uignore        # src-specific rules
    secrets/
      .uignore      # fine-grained rules for this subtree only
```

## CLI reference

### `uignore init [dir] [-i] [--force]`

Creates a `.uignore` file. Without flags, copies a sensible default template.

```bash
uignore init            # create .uignore from template
uignore init --interactive  # guided category selection
uignore init ./myapp    # create in ./myapp
uignore init --force    # overwrite an existing .uignore
```

With `--interactive` (`-i`), you are prompted to include/exclude each category:
secrets, dependencies, build output, lock files, editor noise, and test coverage.

Use `--force` (or `-f`) to overwrite an existing `.uignore` without prompting.

### `uignore add <pattern> [--global]`

Appends a pattern to your `.uignore` (or `~/.uignore` with `--global`). Skips duplicates.

```bash
uignore add secrets/        # add to project .uignore
uignore add *.pem --global  # add to ~/.uignore
```

### `uignore remove <pattern> [--global]`

Removes a pattern from your `.uignore` (or `~/.uignore` with `--global`). Exits with code 1
if the pattern is not found.

```bash
uignore remove secrets/        # remove from project .uignore
uignore remove *.pem --global  # remove from ~/.uignore
```

### `uignore check <file> [dir]`

Tests whether a file would be blocked by the active `.uignore` rules, including `~/.uignore`.

```bash
uignore check secrets/apikey.txt
# ✗ Blocked: "secrets/apikey.txt" matches a rule in .uignore

uignore check src/index.ts
# ✓ Allowed: "src/index.ts"
```

### `uignore list [dir]`

Lists all `.uignore` files found in the project tree and their active rules, including
`~/.uignore` if it exists.

```bash
uignore list
```

```
~/.uignore (2 rules)
  *.pem
  .env

.uignore (3 rules)
  secrets/
  node_modules/
  dist/

src/.uignore (1 rule)
  internal/
```

### `uignore validate [dir]`

Validates all `.uignore` files for syntax errors and warns on patterns that do not match
any git-tracked file (possible typo or obsolete rule). Exits with code 1 if issues found.

```bash
uignore validate
# ✓ .uignore: 5 rules, all valid
# ⚠  .uignore:3: pattern "scret/" does not match any tracked file
```

### `uignore diff [dir] [--all] [--json]`

Shows which git-tracked files are blocked by the active `.uignore` rules. Useful for
auditing rules before rolling out to a team.

```bash
uignore diff         # show only blocked files
uignore diff --all   # show blocked and allowed files
uignore diff --json  # machine-readable JSON output (for CI pipelines)
uignore diff --all --json  # JSON with both blocked and allowed arrays
```

With `--json`, the output format is:

```json
{
  "total": 42,
  "blocked": ["secrets/key.pem"],
  "allowed": ["src/index.ts"]
}
```

The `"allowed"` key is only included when `--all` is also passed.

### `uignore doctor [dir]`

Diagnoses common configuration problems:

- Hook script exists at its compiled path
- `.uignore` file exists and has valid syntax
- Each tool's settings file exists and is valid JSON
- uignore hook is registered for Claude Code and Gemini CLI
- Hook path in settings still points to an existing file

```bash
uignore doctor
# ✓ Hook script found: /usr/local/lib/node_modules/@ottoai/uignore/dist/hook.js
# ✓ .uignore found and syntax valid
# ✓ Claude Code: hook registered and path valid
# ✗ Gemini CLI: hook path no longer exists — run `uignore update` to fix
```

Exits with code 1 if any issues are found.

### `uignore install [dir] [--claude] [--gemini] [--cursor] [--windsurf] [--codex] [--all] [-y] [--dry-run]`

Registers the uignore hook with the specified AI tool(s). Defaults to `--all`.

- If the tool's settings file does not exist, it is created automatically.
- If it already exists, you are prompted to choose:
  - `[1] auto` — merge automatically
  - `[2] manual` — show a snippet to merge yourself

```bash
uignore install              # install for all supported tools
uignore install --claude     # Claude Code only
uignore install --gemini     # Gemini CLI only
uignore install --cursor     # Cursor only
uignore install --windsurf   # Windsurf only
uignore install --codex      # Codex CLI (coming soon)
uignore install --yes        # auto-merge without prompting (CI-friendly)
uignore install --dry-run    # preview what would be written
```

### `uignore update [dir] [--claude] [--gemini] [--cursor] [--windsurf] [--all]`

Re-writes the hook command in each tool's settings file with the current absolute path.
Run this after upgrading `@ottoai/uignore` to fix stale hook paths.

```bash
uignore update          # update all tools
uignore update --claude # Claude Code only
```

### `uignore uninstall [dir] [--claude] [--gemini] [--cursor] [--windsurf] [--codex] [--all]`

Removes the uignore hook from the specified AI tool(s). Defaults to `--all`.

```bash
uignore uninstall              # remove from all supported tools
uignore uninstall --claude     # Claude Code only
uignore uninstall --gemini     # Gemini CLI only
uignore uninstall --cursor     # Cursor only
uignore uninstall --windsurf   # Windsurf only
```

### `uignore status [dir] [--claude] [--gemini] [--cursor] [--windsurf] [--all]`

Shows whether the hook is installed for each tool and lists the active rules from `.uignore`.

```bash
uignore status
```

```
~/.uignore : found (/Users/you/.uignore)
.uignore   : found (/your/project/.uignore)

Claude Code
  Hook installed : yes
  Settings file  : /your/project/.claude/settings.json

Gemini CLI
  Hook installed : yes
  Settings file  : /your/project/.gemini/settings.json

Cursor
  Hook installed : yes
  Settings file  : /your/project/.cursor/hooks.json

Windsurf
  Hook installed : yes
  Settings file  : /your/project/.windsurf/hooks.json

Active rules (5):
  .env
  .env.*
  *.pem
  secrets/
  node_modules/
```

### `uignore install-git-hook [dir]`

Installs a git pre-commit warning hook. Before each commit, it checks whether any staged
files match `.uignore` rules and prints a warning — **without blocking the commit**.

```bash
uignore install-git-hook    # install in current repo
uignore uninstall-git-hook  # remove it
```

### `uignore completions <bash|zsh|fish>`

Prints a shell completion script. Add to your shell config to get tab completion for all
subcommands and flags.

```bash
# bash — add to ~/.bashrc
eval "$(uignore completions bash)"

# zsh — add to ~/.zshrc
eval "$(uignore completions zsh)"

# fish — add to ~/.config/fish/config.fish
uignore completions fish | source
```

### `uignore help [command]`

Shows help for a specific command, or lists all available commands.

```bash
uignore help         # list all commands
uignore help init    # show detailed help for uignore init
uignore help diff    # show detailed help for uignore diff
```

### `uignore version`

Prints the installed version.

```bash
uignore version
# @ottoai/uignore v1.1.0
```

`dir` defaults to the current directory.

## Manual hook configuration

If you prefer to configure the hook yourself, add the following to each tool's settings file.

### Claude Code — `.claude/settings.json`

```json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Read|Edit|Write|Glob|Grep|NotebookEdit",
        "hooks": [
          {
            "type": "command",
            "command": "node /path/to/uignore/dist/hook.js"
          }
        ]
      }
    ]
  }
}
```

### Gemini CLI — `.gemini/settings.json`

```json
{
  "hooks": {
    "BeforeTool": [
      {
        "matcher": "read_file|write_file|replace|read_many_files|grep_search|glob|list_directory",
        "hooks": [
          {
            "type": "command",
            "command": "node /path/to/uignore/dist/hook.js",
            "name": "uignore",
            "description": "Blocks file access matching .uignore rules"
          }
        ]
      }
    ]
  }
}
```

### Cursor — `.cursor/hooks.json`

```json
{
  "version": 1,
  "hooks": {
    "beforeReadFile": [{ "command": "node /path/to/uignore/dist/hook.js" }],
    "beforeTabFileRead": [{ "command": "node /path/to/uignore/dist/hook.js" }],
    "afterFileEdit": [{ "command": "node /path/to/uignore/dist/hook.js" }],
    "afterTabFileEdit": [{ "command": "node /path/to/uignore/dist/hook.js" }]
  }
}
```

### Windsurf — `.windsurf/hooks.json`

```json
{
  "hooks": {
    "pre_read_code": [{ "command": "node /path/to/uignore/dist/hook.js" }],
    "pre_write_code": [{ "command": "node /path/to/uignore/dist/hook.js" }]
  }
}
```

`uignore install` writes all files automatically with the correct absolute path.

## Development

```bash
npm install        # install dependencies
npm run build      # compile TypeScript → dist/
npm test           # run all tests
```

The source is TypeScript. Tests use Node's built-in test runner with `tsx` for on-the-fly
compilation — no separate compile step needed to run tests.

### Project structure

```
src/
  hook.ts                   # PreToolUse / BeforeTool / beforeReadFile / pre_read_code hook
  pre-commit.ts             # git pre-commit warning script
  cli/
    index.ts                # entry point — USAGE + command dispatch
    shared.ts               # shared helpers (parseFlags, loadSettings, …)
    hook-script.ts          # HOOK_SCRIPT path constant
    completions.ts          # bash / zsh / fish completion strings
    commands/
      add.ts                # uignore add
      remove.ts             # uignore remove
      help.ts               # uignore help
      check.ts              # uignore check
      completions.ts        # uignore completions
      diff.ts               # uignore diff
      doctor.ts             # uignore doctor
      git-hook.ts           # uignore install-git-hook / uninstall-git-hook
      init.ts               # uignore init
      install.ts            # uignore install
      list.ts               # uignore list
      status.ts             # uignore status
      uninstall.ts          # uignore uninstall
      update.ts             # uignore update
      validate.ts           # uignore validate
      version.ts            # uignore version
    tools/
      types.ts              # shared interfaces
      claude.ts             # Claude Code hook config
      gemini.ts             # Gemini CLI hook config
      cursor.ts             # Cursor install / uninstall / status / update
      windsurf.ts           # Windsurf install / uninstall / status / update
      codex.ts              # Codex coming-soon stubs
  templates/
    .uignore                # default template created by `uignore init`
  __tests__/
    hook.test.ts            # integration tests
dist/                       # compiled output (tsc)
```