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
38 changes: 18 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,7 @@ or CDN-hosted scripts.

### Requirements

- [`uv`](https://docs.astral.sh/uv) - a Python package and project manager, a modern best-in-class standard for python projects. GrantGuard uses `uv run` as its supported launch path, allowing consistent and convenient execution across MacOS, Linux, and Windows.
- `git` - for cloning this repo
- [`uv`](https://docs.astral.sh/uv) - a Python package and project manager, a modern best-in-class standard for python projects.
- A supported browser for the Web UI

GrantGuard uses browser-native HTML/CSS/JS and is intended for current stable
Expand All @@ -52,15 +51,14 @@ versions of Chrome, Edge, Firefox, and Safari.

0. If you haven't already, [install `uv`](https://github.com/astral-sh/uv#installation).

1. Clone this repo
1. Run GrantGuard directly
```bash
git clone https://github.com/OpenVanta/grantguard.git
uvx --from git+https://github.com/OpenVanta/GrantGuard grantguard
```

2. Run the web UI
That opens the web UI. You can also run the CLI audit the same way:
```bash
cd grantguard
uv run grantguard.py
uvx --from git+https://github.com/OpenVanta/GrantGuard grantguard audit
```

By default, GrantGuard reviews user-level Claude settings sources only. It does
Expand All @@ -72,8 +70,8 @@ project directories unless you ask it to.
### Synopsis

```bash
uv run grantguard.py ui [TARGET ...] [--targets PATH] [--scan | --deep-scan] [--tolerance default|permissive] [--port PORT] [--no-open]
uv run grantguard.py audit [TARGET ...] [--targets PATH] [--scan | --deep-scan] [--tolerance default|permissive] [--show-safe] [--fix]
grantguard ui [TARGET ...] [--targets PATH] [--scan | --deep-scan] [--tolerance default|permissive] [--port PORT] [--no-open]
grantguard audit [TARGET ...] [--targets PATH] [--scan | --deep-scan] [--tolerance default|permissive] [--show-safe] [--fix]
```

### Defaults
Expand All @@ -89,7 +87,7 @@ An empty selection is a successful empty audit. GrantGuard prints that no Claude
settings sources were found and exits `0`.

```bash
uv run grantguard.py audit
grantguard audit
```

### Targets And Scans
Expand All @@ -98,31 +96,31 @@ Targets may be passed positionally or with repeatable `--targets PATH`; both
forms behave identically.

```bash
uv run grantguard.py audit /path/to/repo
uv run grantguard.py audit --targets /path/to/repo
uv run grantguard.py audit --targets /repo/a --targets /repo/b
grantguard audit /path/to/repo
grantguard audit --targets /path/to/repo
grantguard audit --targets /repo/a --targets /repo/b
```

Use `--scan` to shallowly discover `.claude/settings*.json` below one or more
target roots:

```bash
uv run grantguard.py audit --scan --targets /path/to/workspace
grantguard audit --scan --targets /path/to/workspace
```

Use `--deep-scan` for deeper discovery under target roots, or without targets
for broad discovery:

```bash
uv run grantguard.py audit --deep-scan --targets /path/to/workspace
uv run grantguard.py audit --deep-scan
grantguard audit --deep-scan --targets /path/to/workspace
grantguard audit --deep-scan
```

### Tolerance

```bash
uv run grantguard.py audit --tolerance default
uv run grantguard.py audit --tolerance permissive
grantguard audit --tolerance default
grantguard audit --tolerance permissive
```

`default` flags high-risk findings and overbroad wildcard rules. `permissive`
Expand All @@ -133,8 +131,8 @@ keeps overbroad wildcard rules and flags only higher-risk findings.
`audit` is read-only unless `--fix` is present.

```bash
uv run grantguard.py audit --fix
uv run grantguard.py audit --tolerance permissive --fix
grantguard audit --fix
grantguard audit --tolerance permissive --fix
```

`audit --fix` writes to editable Claude settings files in scope and removes all
Expand Down
84 changes: 2 additions & 82 deletions grantguard.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
GrantGuard main entrypoint.
GrantGuard entry point for ``uv run grantguard.py``.

Web UI usage:
uv run grantguard.py ui [TARGET ...] [--targets PATH] [--scan | --deep-scan]
Expand All @@ -9,92 +9,12 @@
uv run grantguard.py audit [TARGET ...] [--targets PATH] [--scan | --deep-scan]
uv run grantguard.py audit --fix # write removals to editable settings files
"""
import argparse
import os
import sys

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))


def handle_serve_ui(args: argparse.Namespace) -> None:
"""Serves the GrantGuard web UI (the `ui` subcommand)."""
from grantguard.server import serve

paths = list(args.paths) + list(args.targets or [])
if args.scan and not paths:
print("grantguard ui: error: --scan requires at least one TARGET or --targets PATH",
file=sys.stderr)
sys.exit(2)

serve(
paths=paths or None,
scan=args.scan,
deep_scan=args.deep_scan,
tolerance=args.tolerance,
port=args.port,
open_browser=not args.no_open,
)


def handle_cli_audit(args: argparse.Namespace) -> None:
"""Invokes the GrantGuard CLI audit (the `audit` subcommand)."""
from grantguard.cli import run_args

sys.exit(run_args(args))


def create_arg_parser() -> argparse.ArgumentParser:
"""Build the top-level parser with `ui`/`audit` subcommands.

Using subparsers (instead of slicing sys.argv by hand) keeps each command's
flags self-describing and lets the parser be exercised in isolation.
"""
from grantguard import cli

parser = argparse.ArgumentParser(
prog="grantguard",
description="🛡️ GrantGuard — audit & clean your Claude Code permission allowlist",
)
sub = parser.add_subparsers(dest="command")

ui = sub.add_parser("ui", help="open the local web UI (the default command)")
ui.add_argument("paths", nargs="*", metavar="TARGET", default=[],
help="settings files or repo/.claude directories to audit")
ui.add_argument("--targets", action="append", default=[], metavar="PATH",
help="add a target path (repeatable)")
ui_scan = ui.add_mutually_exclusive_group()
ui_scan.add_argument("--scan", action="store_true",
help="shallowly discover .claude/settings*.json under target roots")
ui_scan.add_argument("--deep-scan", action="store_true",
help="deeply discover .claude/settings*.json under target roots, "
"or broadly when no targets are provided")
ui.add_argument("--tolerance", choices=("default", "permissive"), default="default")
ui.add_argument("--port", type=int, default=8770)
ui.add_argument("--no-open", action="store_true")
ui.set_defaults(func=handle_serve_ui)

# `audit` reuses cli's flag definitions so the two parsers can't drift apart.
audit = sub.add_parser(
"audit",
help="audit & clean your allowlist (dry run by default)",
description="🛡️ GrantGuard — audit & clean your Claude Code permission allowlist",
)
cli.add_audit_args(audit)
audit.set_defaults(func=handle_cli_audit)

return parser


def main(argv=None) -> None:
argv = list(sys.argv[1:] if argv is None else argv)
# Default to the `ui` subcommand when none is given — a bare `grantguard` or
# an options-first invocation like `grantguard --port 8770` still opens the UI.
if not argv or (argv[0].startswith("-") and argv[0] not in ("-h", "--help")):
argv = ["ui", *argv]

args = create_arg_parser().parse_args(argv)
args.func(args)

from grantguard._entry import main # noqa: E402

if __name__ == "__main__":
main()
83 changes: 83 additions & 0 deletions grantguard/_entry.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"""GrantGuard package entry point.

Provides the ``main`` function used by the ``grantguard`` console script and by
the top-level ``grantguard.py`` shim for ``uv run grantguard.py``.
"""
import argparse
import sys


def handle_serve_ui(args: argparse.Namespace) -> None:
"""Serves the GrantGuard web UI (the `ui` subcommand)."""
from grantguard.server import serve

paths = list(args.paths) + list(args.targets or [])
if args.scan and not paths:
print("grantguard ui: error: --scan requires at least one TARGET or --targets PATH",
file=sys.stderr)
sys.exit(2)

serve(
paths=paths or None,
scan=args.scan,
deep_scan=args.deep_scan,
tolerance=args.tolerance,
port=args.port,
open_browser=not args.no_open,
)


def handle_cli_audit(args: argparse.Namespace) -> None:
"""Invokes the GrantGuard CLI audit (the `audit` subcommand)."""
from grantguard.cli import run_args

sys.exit(run_args(args))


def create_arg_parser() -> argparse.ArgumentParser:
"""Build the top-level parser with `ui`/`audit` subcommands."""
from grantguard import cli

parser = argparse.ArgumentParser(
prog="grantguard",
description="GrantGuard — audit & clean your Claude Code permission allowlist",
)
sub = parser.add_subparsers(dest="command")

ui = sub.add_parser("ui", help="open the local web UI (the default command)")
ui.add_argument("paths", nargs="*", metavar="TARGET", default=[],
help="settings files or repo/.claude directories to audit")
ui.add_argument("--targets", action="append", default=[], metavar="PATH",
help="add a target path (repeatable)")
ui_scan = ui.add_mutually_exclusive_group()
ui_scan.add_argument("--scan", action="store_true",
help="shallowly discover .claude/settings*.json under target roots")
ui_scan.add_argument("--deep-scan", action="store_true",
help="deeply discover .claude/settings*.json under target roots, "
"or broadly when no targets are provided")
ui.add_argument("--tolerance", choices=("default", "permissive"), default="default")
ui.add_argument("--port", type=int, default=8770)
ui.add_argument("--no-open", action="store_true")
ui.set_defaults(func=handle_serve_ui)

# `audit` reuses cli's flag definitions so the two parsers can't drift apart.
audit = sub.add_parser(
"audit",
help="audit & clean your allowlist (dry run by default)",
description="GrantGuard — audit & clean your Claude Code permission allowlist",
)
cli.add_audit_args(audit)
audit.set_defaults(func=handle_cli_audit)

return parser


def main(argv=None) -> None:
argv = list(sys.argv[1:] if argv is None else argv)
# Default to the `ui` subcommand when none is given — a bare `grantguard` or
# an options-first invocation like `grantguard --port 8770` still opens the UI.
if not argv or (argv[0].startswith("-") and argv[0] not in ("-h", "--help")):
argv = ["ui", *argv]

args = create_arg_parser().parse_args(argv)
args.func(args)
5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ requires-python = ">=3.10"
license = { text = "MIT" }
dependencies = []

[tool.uv]
package = false
[project.scripts]
grantguard = "grantguard._entry:main"