You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit fbac33c
Browse filesBrowse the repository at this point in the historyBrowse files
Copy file name to clipboardExpand all lines: CLAUDE.md
+34-4Lines changed: 34 additions & 4 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -10,13 +10,24 @@ bun run bashful.ts <command> # Wrap a command using --hel
10
10
bun run bashful.ts curl \| wget # Multiple commands, one endpoint each
11
11
bun run bashful.ts curl --help \| wget --help # Explicit help commands (pipe mode)
12
12
bun run bashful.ts --debug curl \| wget # Run with debug logging
13
+
bun run bashful.ts --config policy.json curl # Run with an access-control policy
13
14
bun run start # Alias: wraps curl
14
15
```
15
16
16
17
```bash
17
18
bun test# Run all tests
19
+
bun test -t "authorizeFlags"# Run one describe block / test by name
20
+
bunx tsc --noEmit # Typecheck (tsconfig is noEmit; there is no build step)
18
21
```
19
22
23
+
CI (`.github/workflows/test.yml`) runs `bun install` + `bun test` on push/PR to `main`. Lint is not wired into `package.json` — ESLint, gitleaks, and whitespace fixers run via `.pre-commit-config.yaml`.
`docs/usage.md` (invocation, endpoints, payloads, access control), `docs/architecture.md` (schema synthesis, enforcement layering, invariants), `docs/testing.md` (suite layout, integration-test mechanics). Keep them in sync when changing behaviour they describe.
30
+
20
31
## Architecture
21
32
22
33
The entire application lives in a single file: `bashful.ts`.
@@ -25,19 +36,38 @@ The entire application lives in a single file: `bashful.ts`.
25
36
26
37
1.**Ingestion** — Executes `<command> --help` (or `<explicit command>` in `pipe` mode) via `Bun.spawnSync` and captures stdout+stderr.
27
38
2.**Schema synthesis** — Parses the help text with a heuristic regex (the "Bashful Regex") that extracts short flags (`-x`), long flags (`--foo`), argument types (`<val>`, `[val]`, or `ALL_CAPS`), and descriptions into a `schema` object keyed by flag name.
28
-
3.**Server** — Spins up a `Bun.serve` HTTP server on port 3000 with three routes:
39
+
3.**Policy** — Loads an optional JSON config (`--config <file>`, `$BASHFUL_CONFIG`, or `./bashful.config.json`) and refuses to wrap any denied command. Denied flags are stripped from the served schema so the UI can't offer them.
40
+
4.**Server** — Spins up a `Bun.serve` HTTP server on port 3000 with three routes:
29
41
-`GET /` (also `/docs`, `/ui`) — Returns a self-contained HTML page (the Swagger-like UI) with the schema baked in via template literal.
30
42
-`GET /<command>/schema` — Returns the parsed schema as JSON.
31
-
-`POST /<command>` or `GET /<command>` — Translates the JSON body (or query params) back into CLI arguments and executes the command with `Bun.spawn`, streaming stdout as the response.
43
+
-`POST /<command>` or `GET /<command>` — Checks the payload against the policy (403 with a `reason` if blocked), then translates the JSON body (or query params) back into CLI arguments and executes the command with `Bun.spawn`, streaming stdout as the response.
32
44
33
45
**Payload conventions (POST `/<command>`):**
34
46
-`_args`: positional arguments (string or string array)
47
+
-`_stdin`: string piped to the command's stdin. Absent → stdin is closed, not inherited (else a stdin-reading tool hangs).
- Value flags: `{ "output": "file.html" }` → `--output file.html`
37
-
- Unknown single-char keys fall back to `-x` short-flag style.
50
+
- Array values repeat the flag: `{ "header": ["a", "b"] }` → `--header a --header b`
51
+
- Unknown single-char keys fall back to `-x` short-flag style; objects are rejected with 400.
52
+
-`Accept: application/json` buffers and returns `{exitCode, stdout, stderr, timedOut}` instead of streaming.
38
53
39
54
**Multiple commands:** Separate commands with `\|` (escaped pipe character). Each segment becomes its own endpoint and tab in the UI.
40
55
-`bashful.ts curl \| wget` — two endpoints: `/curl` and `/wget`
41
56
-`bashful.ts curl --help \| wget --help` — pipe mode per segment: runs the full command as-is to get help text (useful when `--help` alone fails or outputs to stderr)
42
57
43
-
**`--debug` flag:** logs startup time, number of parsed flags, and each execution command.
58
+
**Access control:** an optional config file gates commands, flags, and flag values. `mode` is `blacklist` (allow unless denied) or `whitelist` (deny unless allowed). `commands.allow`/`deny` gate whole commands; `flags.<cmd>.allow`/`deny` gate individual flags; `flags.<cmd>.denyCombinations`/`allowCombinations` gate *sets* of flags used together; `flags.<cmd>.values` maps a flag to a regex its value must match. The `"*"` key under `flags` applies to every command; `"*"` inside a list means "everything". Rules name payload keys (`output`, `_args`), not CLI spellings (`--output`). Deny always beats allow. See `bashful.config.example.json` and `docs/usage.md`.
59
+
60
+
**`--debug` flag:** logs startup time, number of parsed flags, config load, blocked requests, and each execution command.
61
+
62
+
## Conventions
63
+
64
+
**Pure core, imperative shell.** Everything above the `// ── Entry point ──` divider in `bashful.ts` is exported pure functions (`splitSegments`, `parseSchema`, `buildCLIArgs`, `normalizeConfig`, `authorizeCommand`, `authorizeFlags`, `filterSchema`, …); everything below runs under `if (import.meta.main)`. Tests import the module directly, so new logic belongs above the divider — anything below it only runs as a process and can't be unit-tested.
65
+
66
+
**Invariants that are easy to undo accidentally:**
67
+
- The server binds to `127.0.0.1` by default. It executes arbitrary CLI commands, so it must not listen on `0.0.0.0` unless the operator deliberately sets `HOST`.
68
+
-**Browser hardening works only as a set** — no CORS headers unless `--allow-origin` names one; exec requires `POST` + `Content-Type: application/json` (this is what forces a preflight, which then fails); the `Host` header must be loopback (defeats DNS rebinding); `GET` exec is off unless `--allow-get`. Loosening any single one re-opens command execution to any web page the user visits. See `docs/architecture.md#invariants`.
69
+
- The exec route merges stdout **and** stderr into one stream. Many CLIs write their real output or diagnostics to stderr; returning stdout alone silently yields empty responses.
70
+
71
+
**Tests** (`bashful.test.ts`) are unit tests plus integration tests that spawn the real server via `bun run ./bashful.ts` on fixed ports (3005–3009). New integration suites need their own unused port, and policy fixtures are written to `tmpdir()` and passed with `--config`.
72
+
73
+
**Windows:**`safeSpawn` retries a failed `ENOENT` spawn through `cmd /c`, which is how shell builtins and `.cmd` shims resolve. The `\|` command separator is escaped because an unescaped `|` would be consumed by the shell.
-**[Architecture](docs/architecture.md)** — how the help text becomes a schema, how a payload becomes a command line, and the invariants that keep it safe.
42
+
-**[Testing](docs/testing.md)** — how the suite is organized and how to add to it.
43
+
44
+
---
45
+
38
46
## Prerequisites
39
47
40
48
-[Bun](https://bun.sh/) v1.0+
@@ -84,6 +92,9 @@ Open [http://localhost:3000](http://localhost:3000) in your browser to view the
84
92
> The POST above translates dynamically to the native CLI invocation:
> Bashful executes real commands, so it is hardened against the browser: no CORS by default, exec requires `POST` with `Content-Type: application/json`, the `Host` header must be loopback, and `GET` execution is off unless you pass `--allow-get`. See [Browser safety](docs/usage.md#browser-safety).
97
+
87
98
---
88
99
89
100
## Multiple commands
@@ -134,6 +145,65 @@ bun run bashful.ts curl --help \| wget
134
145
135
146
---
136
147
148
+
## Access control (whitelist / blacklist)
149
+
150
+
Bashful executes real commands, so you can restrict what it will run with an optional JSON config — both **which commands** are wrappable and **which flags (and combinations of flags)** each command accepts.
151
+
152
+
Bashful loads its config from, in order: `--config <file>`, `$BASHFUL_CONFIG`, or `./bashful.config.json` if it exists. With no config, nothing is restricted and behaviour is unchanged.
153
+
154
+
```bash
155
+
bun run bashful.ts --config bashful.config.json curl \| wget
156
+
```
157
+
158
+
See [`bashful.config.example.json`](bashful.config.example.json) for a working starting point.
|`mode`|`"blacklist"` (default) — everything is allowed unless denied. `"whitelist"` — nothing is allowed unless explicitly allowed. |
185
+
|`commands.allow` / `commands.deny`| Which commands may be wrapped at all. |
186
+
|`flags.<cmd>`| Flag rules for one command. The `"*"` key applies to every command and is merged with the command's own rules. |
187
+
|`flags.<cmd>.allow` / `.deny`| Individual flags. Naming an `allow` list whitelists that command's flags even in blacklist mode. |
188
+
|`flags.<cmd>.denyCombinations`| List of flag sets. A request is rejected if it uses **all** flags of any listed set — the flags remain fine on their own. |
189
+
|`flags.<cmd>.allowCombinations`| List of flag sets. A request is rejected unless every flag it uses fits inside **one** listed set. |
190
+
|`flags.<cmd>.values`| Flag → regex its value must match, e.g. `{"output": "^/tmp/", "_args": "^https://api\\.example\\.com/"}`. Allowing a flag doesn't constrain it; this does. |
191
+
192
+
Rules use **payload key names**, not CLI spellings: write `output`, not `--output`. Positional arguments are governed under the name `_args`, and `"*"` in any list means "everything". Full reference: [docs/usage.md](docs/usage.md#access-control).
193
+
194
+
**Deny always beats allow.** A flag set to `false` builds to nothing, so it is ignored by the rules.
195
+
196
+
### How it is enforced
197
+
198
+
-**At startup** — wrapping a denied command is refused and Bashful exits with a non-zero status.
199
+
-**At request time** — a blocked payload gets `403 Forbidden` with a JSON `reason`, and the command never runs. This covers both `POST` bodies and `GET` query params, including keys that never appeared in the parsed schema.
200
+
-**In the schema and UI** — `GET /<cmd>/schema` only advertises the flags the policy permits, so the generated form can't offer a flag that would be rejected. Combination rules can't be expressed in a form, so those are enforced on the request.
201
+
202
+
> [!WARNING]
203
+
> This gates the flags Bashful passes to a command; it does not sandbox the command itself. A wrapped tool that can read files or reach the network can still do so within the flags you permit. Bashful binds to `127.0.0.1` by default for the same reason.
204
+
205
+
---
206
+
137
207
## Debug mode
138
208
139
209
Pass `--debug` anywhere before the command to log startup time, parsed flag counts, and each execution:
@@ -147,10 +217,12 @@ bun run bashful.ts --debug curl \| wget
147
217
## Running tests
148
218
149
219
```bash
150
-
bun test
220
+
bun test# everything
221
+
bun test -t "authorizeFlags"# a single describe block or test
222
+
bunx tsc --noEmit # typecheck (no build step)
151
223
```
152
224
153
-
Tests cover the three pure functions at the core of Bashful: `splitSegments` (arg parsing), `parseSchema` (regex-based help text parsing), and `buildCLIArgs` (payload → CLI translation).
225
+
Tests cover the pure functions at the core of Bashful — `splitSegments` (arg parsing), `parseSchema` (regex-based help text parsing), `buildCLIArgs` (payload → CLI translation), and the access-control layer (`parseConfig`, `authorizeCommand`, `authorizeFlags`, `filterSchema`) — plus integration tests that run the real server and check routing and policy enforcement end to end. See [docs/testing.md](docs/testing.md).
154
226
155
227
---
156
228
@@ -161,9 +233,12 @@ Everything lives in a single file: `bashful.ts`.
161
233
-**`splitSegments(args)`** — splits CLI args on `|` into per-command segments.
162
234
-**`parseSchema(helpText)`** — the "Bashful Regex" extracts short flags, long flags, value types (`<val>`, `[val]`, `ALL_CAPS`), and descriptions into a keyed schema object.
163
235
-**`buildCLIArgs(payload, schema)`** — translates a JSON payload back into a flat CLI argument array.
236
+
-**Access control** — `parseConfig` validates the policy file; `authorizeCommand` / `authorizeFlags` decide what may run; `filterSchema` hides forbidden flags from the schema and UI.
164
237
-**Server** — `Bun.serve` on port 3000. Routes: `GET /` (UI), `GET /<cmd>/schema`, `POST /<cmd>`.
165
238
-**Execution** — `Bun.spawn` runs the real command and streams stdout directly as the HTTP response.
166
239
240
+
Deeper dive — including the Bashful Regex, the enforcement layering, and the invariants worth not breaking: [docs/architecture.md](docs/architecture.md).
0 commit comments