Skip to content
This repository was archived by the owner on Jun 28, 2026. It is now read-only.

Commit 713758a

Browse files
committed
docs(approval-hub): architecture, security, release pipeline
Add ARCHITECTURE.md and SECURITY.md targeting the future public repo (kanywst/approval-hub), plus goreleaser config and release workflow so v0.x.x tags produce multi-platform binaries and a Homebrew tap entry.
1 parent 06662f2 commit 713758a

4 files changed

Lines changed: 240 additions & 0 deletions

File tree

.github/workflows/release.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags:
6+
- "v*"
7+
8+
permissions:
9+
contents: write
10+
11+
jobs:
12+
goreleaser:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v4
16+
with:
17+
fetch-depth: 0
18+
- uses: actions/setup-go@v5
19+
with:
20+
go-version: "1.26"
21+
- uses: goreleaser/goreleaser-action@v6
22+
with:
23+
version: latest
24+
args: release --clean
25+
env:
26+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
27+
TAP_GITHUB_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }}

.goreleaser.yaml

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
version: 2
2+
3+
before:
4+
hooks:
5+
- go mod tidy
6+
7+
builds:
8+
- id: approval-hub
9+
main: ./cmd/approval-hub
10+
binary: approval-hub
11+
env:
12+
- CGO_ENABLED=0
13+
goos:
14+
- darwin
15+
- linux
16+
goarch:
17+
- amd64
18+
- arm64
19+
ldflags:
20+
- -s -w
21+
- -X main.version={{ .Version }}
22+
- -X main.commit={{ .Commit }}
23+
- -X main.date={{ .Date }}
24+
25+
archives:
26+
- id: default
27+
formats: [tar.gz]
28+
name_template: >-
29+
{{ .ProjectName }}_
30+
{{- .Os }}_
31+
{{- .Arch }}
32+
33+
checksum:
34+
name_template: "checksums.txt"
35+
36+
snapshot:
37+
version_template: "{{ incpatch .Version }}-next"
38+
39+
changelog:
40+
sort: asc
41+
filters:
42+
exclude:
43+
- "^docs:"
44+
- "^chore:"
45+
- "^test:"
46+
47+
brews:
48+
- name: approval-hub
49+
repository:
50+
owner: kanywst
51+
name: homebrew-tap
52+
homepage: https://github.com/kanywst/approval-hub
53+
description: Aggregate Claude Code permission prompts into one TUI.
54+
license: MIT
55+
install: |
56+
bin.install "approval-hub"
57+
test: |
58+
system "#{bin}/approval-hub", "--help"

ARCHITECTURE.md

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# Architecture
2+
3+
approval-hub is a local HTTP broker daemon plus a Bubble Tea TUI client,
4+
both packaged in a single Go binary (`approval-hub`). A thin Claude Code
5+
plugin distributed separately wires each Claude Code session's `PreToolUse`
6+
hook to the broker via `type: "http"`.
7+
8+
## Process layout
9+
10+
```text
11+
+------------------+ +------------------+
12+
| claude session A | | claude session B |
13+
| PreToolUse:http | | PreToolUse:http |
14+
+--------+---------+ +--------+---------+
15+
| |
16+
| POST /decide | POST /decide
17+
| Bearer <token> | Bearer <token>
18+
+-------------+-------------+
19+
v
20+
+---------------------+
21+
| broker daemon |
22+
| 127.0.0.1:17456 |
23+
| |
24+
| - decision engine |
25+
| - matcher store |
26+
| - pending queue |
27+
| - SSE /events |
28+
+---------+-----------+
29+
| SSE /events
30+
v
31+
+---------------------+
32+
| TUI client |
33+
| approval-hub attach|
34+
+---------------------+
35+
```
36+
37+
Three processes are loosely coupled over HTTP and SSE. If the TUI or the
38+
daemon dies, Claude sessions keep working: the hook receives a non-2xx /
39+
connection error and Claude Code falls back to its built-in interactive
40+
prompt. This is guaranteed by Claude Code's hook semantics.
41+
42+
## Broker daemon
43+
44+
Single goroutine-safe HTTP server on `127.0.0.1`.
45+
46+
- `POST /decide` — PreToolUse entry. Generates a permission-rule-syntax
47+
matcher (`Bash(npm test:*)`, `Edit(/path/to/file)`, ...) from the tool
48+
input, looks it up in the matcher store. Cache hit returns immediately;
49+
cache miss enqueues a pending request and waits up to
50+
`ui_timeout_seconds` for a TUI decision, after which it returns `defer`.
51+
- `GET /events` — Server-Sent Events stream. Publishes
52+
`pending_added` / `pending_resolved` / `pending_expired` / `rule_added`
53+
/ `rule_revoked`. Slow subscribers drop events when their buffer fills.
54+
- `GET /pending`, `POST /pending/{id}/resolve` — list and resolve.
55+
- `GET /rules`, `DELETE /rules/{id}` — inspect and tombstone matchers.
56+
- `POST /rotate-token` — generate a new bearer token and persist it.
57+
58+
Bearer-token middleware in front of every route. Rate limiter uses a
59+
per-second token bucket sized by `rate_limit_per_second`.
60+
61+
## TUI client
62+
63+
Bubble Tea Model with an SSE consumer goroutine. Reconnects with
64+
exponential backoff up to 30 s. Decisions go back to the broker as
65+
`POST /pending/{id}/resolve` with `scope` of `once` (no learning),
66+
`persist` (matcher appended to the store), or `ttl` (matcher with an
67+
`expires_at`).
68+
69+
Session IDs are SHA-1 hashed to one of 8 ANSI colors so requests from
70+
the same session group visually.
71+
72+
## Matcher store
73+
74+
Append-only JSONL log at `${data_dir}/learned-rules.jsonl`. Each line is
75+
either a `rule_added` record (with optional `expires_at`) or a
76+
`rule_revoked` tombstone referencing a previous rule's ID. The broker
77+
reconstructs an in-memory map on startup.
78+
79+
Matcher strings follow Claude Code's `if` field syntax so the rule set
80+
can be exported to `settings.json`'s `permissions.allow` list when the
81+
broker is uninstalled.
82+
83+
## Failure modes
84+
85+
- **broker not running** — hook receives connection refused → Claude
86+
Code falls back to manual prompt
87+
- **broker crashes mid-pending** — hook hits its own `timeout`
88+
Claude Code falls back
89+
- **TUI client crashes** — broker keeps queueing pending; next attach
90+
replays via `GET /pending` and SSE
91+
- **pending queue full** — broker returns 503; Claude Code falls back
92+
- **disk full (store)**`AddRule` errors; resolve still returns the
93+
decision in-memory
94+
95+
## Configuration
96+
97+
`config.json` in the OS-standard data directory
98+
(`~/Library/Application Support/approval-hub` on macOS,
99+
`~/.config/approval-hub` on Linux, or `$APPROVAL_HUB_DATA`).
100+
101+
```json
102+
{
103+
"port": 17456,
104+
"token": "ahub_<64hex>",
105+
"ui_timeout_seconds": 60,
106+
"max_pending_requests": 100,
107+
"rate_limit_per_second": 50
108+
}
109+
```
110+
111+
File permissions are `0600`. The token is embedded into the Claude Code
112+
plugin's `hooks/hooks.json` by the `install` skill.

SECURITY.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Security
2+
3+
## Threat model
4+
5+
approval-hub runs as the same OS user as the Claude Code session it
6+
brokers for, on the local machine only.
7+
8+
### In scope
9+
10+
- An attacker on the same network attempting to send `/decide` requests
11+
to the broker → mitigated by binding only to `127.0.0.1`.
12+
- A passive observer on the local network → mitigated by `127.0.0.1`
13+
only; no TLS is needed.
14+
- A program owned by a different OS user reading the token or sending
15+
requests → mitigated by `0600` permissions on `config.json` and
16+
`hooks/hooks.json`.
17+
18+
### Out of scope
19+
20+
- Other processes running as the same OS user. They can read
21+
`config.json` and impersonate the broker or the TUI. Defense at this
22+
layer is the OS user account itself.
23+
- Root-level malware. If the system is rooted, all bets are off.
24+
- Bugs in Claude Code itself.
25+
26+
## Mitigations
27+
28+
- Bind only to `127.0.0.1`; never to `0.0.0.0` or a Unix socket exposed
29+
to other users.
30+
- Bearer token (`crypto/rand` 32 bytes, hex-encoded with `ahub_` prefix).
31+
- `0600` on `config.json`, `hooks/hooks.json`, `learned-rules.jsonl`,
32+
and `approval-hub.pid`.
33+
- Constant-time token comparison (`crypto/subtle.ConstantTimeCompare`).
34+
- Token rotation via `approval-hub rotate` (also rewrites the plugin's
35+
`hooks.json`).
36+
- Rate limiter (`rate_limit_per_second`) and max-pending bound
37+
(`max_pending_requests`) to slow brute-force or DoS attempts even
38+
from the same user.
39+
40+
## Reporting vulnerabilities
41+
42+
Please open a GitHub issue tagged `security` or email the maintainer
43+
listed in the README. Do not disclose publicly until a fix is released.

0 commit comments

Comments
 (0)