Metadata-Version: 2.4
Name: saferead
Version: 0.1.2
Summary: Claude Code PreToolUse hook that auto-approves known-safe read-only shell commands.
License-Expression: GPL-3.0-or-later
License-File: LICENSE
Requires-Python: >=3.11
Requires-Dist: pglast==7.14
Description-Content-Type: text/markdown

# saferead

A [Claude Code](https://claude.com/claude-code) `PreToolUse` hook that
auto-approves known-safe, read-only shell commands — so routine reads
(`ls`, `cat`, `git status`, `grep`, …) don't interrupt you with permission
prompts.

[![PyPI](https://img.shields.io/pypi/v/saferead)](https://pypi.org/project/saferead/)
![Python](https://img.shields.io/badge/python-3.11%2B-blue)
[![License](https://img.shields.io/badge/license-GPL--3.0--or--later-blue)](LICENSE)

## What it does

`saferead` inspects each `Bash` command Claude Code is about to run and returns
one of three verdicts:

- **allow** — every part of the command is a recognized read-only operation.
- **ask** — a gated git write (`add` / `commit` / `stash`) on a protected
  branch (`master` / `main`); you get a confirmation prompt.
- **abstain** — anything it doesn't recognize: it stays silent and lets Claude
  Code's normal permission flow proceed.

It **never denies** a command. It only ever *removes* friction (allow) or
*adds* a confirmation (ask), so it composes safely alongside any separate
command-blocking layer you run.

Compound commands are decomposed at top-level `&&`, `||`, `;`, `|`, and
newlines (quote / backtick / `$(…)`-aware). A command is allowed **only when
every segment** is a recognized safe read.

### Design guarantee

The cardinal rule is **zero false-allows**: a command that mutates state must
never be silently approved. Coverage is secondary — when in doubt, it abstains.
The hook is also built to **never crash or block** Claude Code: any internal
error is caught and the hook abstains.

## Examples

| Command | Verdict | Why |
|---------|---------|-----|
| `ls -la`, `cat README.md`, `grep -rn foo src` | **allow** | read-only |
| `git status`, `git log --oneline`, `git diff` | **allow** | read-only git |
| `cat a.txt && wc -l a.txt` | **allow** | every segment is a safe read |
| `git commit -m "wip"` on `main` | **ask** | gated write on a protected branch |
| `git add .` on a feature branch | **allow** | gated write off a protected branch |
| `rm -rf build` | **abstain** | not recognized → normal prompt |
| `npm install` | **abstain** | not a read |
| `cat a.txt && rm b.txt` | **abstain** | one segment mutates state |

Only the `Bash` tool is inspected; every other tool is left untouched.

## Requirements

[uv](https://docs.astral.sh/uv/) is the only prerequisite. `uvx` fetches and
runs `saferead` on demand (cached after first use) and provides the Python
3.11+ runtime — there is nothing to install.

## Enable the hook

Add `saferead` as a `PreToolUse` hook in your Claude Code settings —
`~/.claude/settings.json` (all projects) or a project's `.claude/settings.json`:

```json
{
  "hooks": {
    "PreToolUse": [
      { "matcher": "Bash", "hooks": [ { "type": "command", "command": "uvx saferead" } ] }
    ]
  }
}
```

That's it. On each `Bash` command `uvx` runs `saferead`, which reads the
PreToolUse payload on stdin and returns a decision. To pin a version, use
`uvx saferead@0.1.1`.

## Configuration

Configuration is optional — `saferead` ships with safe built-in defaults.
Two TOML files are read, both optional:

| File | Trust | Purpose |
|------|-------|---------|
| `~/.config/saferead/config.toml` | trusted | your global settings |
| `$CLAUDE_PROJECT_DIR/.claude/saferead.toml` | untrusted | per-project overrides |

The project file can only **narrow** trust (add protected branches, disable
recognizers) — it can never broaden the allow-set for git writes, by design.

Every key is optional; the values below are the **built-in defaults**:

```toml
[git]
protected_branches = ["master", "main"]           # ask on a gated git write on these branches
gated_subcommands  = ["add", "commit", "stash"]    # which git writes are gated

[recognizers]
disabled = []   # recognizer tags to turn off (those commands then abstain)

[python]
allowed_modules = ["math", "datetime", "json"]  # modules the `python -c` analyzer admits
allowed_methods = []                            # extra read-only methods to admit

[read]
local_allowed_roots = []   # empty = allow any path; non-empty restricts file reads to these roots
ssh_allowed_roots   = []

[logging]
enabled = true                       # audit log of every allow/ask decision (ON by default)
path = "/tmp/claude-hook-audit.log"  # where that audit log is written
```

### Logs

`saferead` writes two files under `/tmp`:

- **Audit log** (`/tmp/claude-hook-audit.log`) — one JSON line per allow/ask
  decision. **On by default**; configure or disable it via the `[logging]`
  table above.
- **Error log** (`/tmp/claude-hook.log`) — internal diagnostics if the hook
  ever hits an error (it always abstains rather than crashing). Fixed path,
  not configurable.

## Updating

Unpinned (`uvx saferead`) runs from uv's cache. Pull the newest release with:

```bash
uvx --refresh saferead
```

Or pin the version in your hook command (`uvx saferead@<version>`) and bump it
when you want to upgrade.

## License

[GPL-3.0-or-later](LICENSE)
