bashboy 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,23 @@
1
+ name: release
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ publish:
12
+ runs-on: ubuntu-latest
13
+ environment: pypi
14
+ permissions:
15
+ id-token: write # OIDC for PyPI trusted publishing
16
+ contents: read
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ - uses: astral-sh/setup-uv@v6
20
+ - name: Build sdist and wheel
21
+ run: uv build
22
+ - name: Publish to PyPI
23
+ run: uv publish --trusted-publishing always
@@ -0,0 +1,25 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .venv/
6
+ venv/
7
+ dist/
8
+ build/
9
+
10
+ # Tooling
11
+ .pytest_cache/
12
+ .ruff_cache/
13
+ .mypy_cache/
14
+ .coverage
15
+ htmlcov/
16
+
17
+ # Local state
18
+ .bashboy/
19
+ *.log
20
+
21
+ # OS / editors
22
+ .DS_Store
23
+ Thumbs.db
24
+ .idea/
25
+ .vscode/
bashboy-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mann10
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
bashboy-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,298 @@
1
+ Metadata-Version: 2.5
2
+ Name: bashboy
3
+ Version: 0.1.0
4
+ Summary: Classify shell commands as allow or deny before an agent runs them.
5
+ Project-URL: Homepage, https://github.com/Mann10/bashboy
6
+ Project-URL: Issues, https://github.com/Mann10/bashboy/issues
7
+ Project-URL: Source, https://github.com/Mann10/bashboy
8
+ Author-email: Mann10 <82712171+Mann10@users.noreply.github.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: agents,claude-code,guardrails,opencode,security,shell
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Security
15
+ Requires-Python: >=3.10
16
+ Description-Content-Type: text/markdown
17
+
18
+ # BashBoy
19
+
20
+ BashBoy is a small, dependency-free Python CLI that classifies a shell command as
21
+ `allow` or `deny` **before** a coding agent runs it. It sits in front of Claude Code
22
+ (`PreToolUse` hook) and OpenCode (plugin) and answers one question: is this command
23
+ safe to run unattended?
24
+
25
+ > **BashBoy only narrows; `allow` means *no objection*.** It never approves a tool
26
+ > call. When BashBoy says `allow` it prints nothing (Claude) or returns normally
27
+ > (OpenCode), and the host's normal permission flow still applies. There is no
28
+ > `ask` verdict, no auto-approve path, and no way for BashBoy to widen permissions.
29
+
30
+ Three layers, in order:
31
+
32
+ 1. **Local fast paths** (no network): a structural allowlist for single read-only
33
+ commands, and a hard denylist for destructive ones.
34
+ 2. **Model classification** against the Clocked (`clocked/fill`) endpoint for everything
35
+ else.
36
+ 3. **Host adapters** that express denial only.
37
+
38
+ Anything the fast paths do not recognise goes to the model. Anything the model path
39
+ cannot answer clearly becomes `deny`.
40
+
41
+ ## Install
42
+
43
+ Requires Python 3.10+ and [uv](https://docs.astral.sh/uv/).
44
+
45
+ ```bash
46
+ uv tool install bashboy # install once
47
+ uvx bashboy --help # or run without installing
48
+ ```
49
+
50
+ From a local checkout (uvx resolves the project from the path):
51
+
52
+ ```bash
53
+ uvx --from /path/to/BashBoy bashboy check --command "git status"
54
+ uvx --from /path/to/BashBoy bashboy install --claude
55
+ ```
56
+
57
+ The package has **zero runtime dependencies** — everything is the Python standard
58
+ library. `pytest` is a development-only dependency.
59
+
60
+ The API key is read from the environment (default `CLOCKED_API_KEY`). It is never
61
+ written to disk:
62
+
63
+ ```bash
64
+ export CLOCKED_API_KEY=... # bash
65
+ $env:CLOCKED_API_KEY = "..." # PowerShell
66
+ ```
67
+
68
+ ## Quick start
69
+
70
+ ```bash
71
+ bashboy check --command "rm -rf /"
72
+ # deny (denylist): recursive delete of a protected path
73
+
74
+ bashboy check --command "git status"
75
+ # allow (allowlist): matches allowlist: git status
76
+
77
+ bashboy check --command "docker run --rm alpine sh" --json
78
+ # {"decision": "deny", "source": "fallback", "reason": "classifier unavailable: no API key ...", ...}
79
+ ```
80
+
81
+ Exit codes:
82
+
83
+ | Code | Meaning |
84
+ |------|---------|
85
+ | `0` | `allow` |
86
+ | `1` | `deny` |
87
+ | `2` | configuration or usage error (never a silent allow) |
88
+
89
+ `--json` prints exactly one object, in this key order:
90
+
91
+ ```json
92
+ {"decision":"deny","source":"denylist","reason":"recursive delete of a protected path","risk":null,"latency_ms":0}
93
+ ```
94
+
95
+ `source` is one of `denylist`, `allowlist`, `model`, `fallback`, `disabled`.
96
+ `--cwd DIR` supplies the working directory used for context (default: current
97
+ directory).
98
+
99
+ ## Configuration
100
+
101
+ Optional file at `~/.bashboy/config.toml` (override with `BASHBOY_CONFIG`).
102
+ Create it with `bashboy config init`; inspect with `bashboy config show`.
103
+ A missing file means these defaults:
104
+
105
+ ```toml
106
+ [provider]
107
+ endpoint = "https://clocked.rezolve.ai/clocked/fill"
108
+ api_key_env = "CLOCKED_API_KEY"
109
+ timeout_ms = 4000
110
+
111
+ [policy]
112
+ fail_mode = "closed" # closed | open
113
+ denylist = true
114
+ allowlist = true
115
+
116
+ [integrations.claude]
117
+ timeout_s = 15
118
+
119
+ [integrations.opencode]
120
+ timeout_ms = 15000
121
+ ```
122
+
123
+ Malformed TOML is a hard error (exit 2) from `check` — the gate never silently
124
+ loosens because a file failed to parse.
125
+
126
+ | Variable | Effect |
127
+ |----------|--------|
128
+ | `BASHBOY_CONFIG` | Path to the config file (default `~/.bashboy/config.toml`) |
129
+ | `BASHBOY_ENDPOINT` | Overrides `[provider].endpoint` |
130
+ | `BASHBOY_API_KEY_ENV` | Name of the environment variable holding the key |
131
+ | `BASHBOY_FAIL_MODE` | `closed` (default) or `open`; anything else is an error |
132
+ | `BASHBOY_DISABLE` | `1`/`true`/`yes` disables **all** checks (see below) |
133
+ | `BASHBOY_DEBUG` | `1`/`true`/`yes` writes diagnostics to stderr only |
134
+
135
+ ## Claude Code setup
136
+
137
+ ```bash
138
+ bashboy install --claude
139
+ ```
140
+
141
+ This merges a `PreToolUse` hook into `~/.claude/settings.json` (override with
142
+ `--settings PATH`). Unrelated keys and unrelated hook events are preserved; an
143
+ existing BashBoy handler is updated in place, so re-running never duplicates it.
144
+ The previous file is copied to `settings.json.bak` and the new file is written
145
+ atomically.
146
+
147
+ The exact entry it writes:
148
+
149
+ ```json
150
+ {
151
+ "hooks": {
152
+ "PreToolUse": [
153
+ {
154
+ "matcher": "Bash|PowerShell",
155
+ "hooks": [
156
+ {
157
+ "type": "command",
158
+ "command": "uvx",
159
+ "args": ["bashboy", "check", "--claude-hook"],
160
+ "timeout": 15,
161
+ "statusMessage": "BashBoy: checking command"
162
+ }
163
+ ]
164
+ }
165
+ ]
166
+ }
167
+ }
168
+ ```
169
+
170
+ Options:
171
+
172
+ ```bash
173
+ bashboy install --claude --dry-run # print the resulting JSON, write nothing
174
+ bashboy install --claude --settings ./settings.json
175
+ bashboy install --claude --from /path/to/BashBoy
176
+ ```
177
+
178
+ `--from SPEC` rewrites the argv to
179
+ `["--from", SPEC, "bashboy", "check", "--claude-hook"]`, so `uvx` runs the given
180
+ local checkout. The path is written verbatim as a single argv element — never
181
+ interpolated into a shell string.
182
+
183
+ The hook reads one JSON object on stdin. On `deny` it prints the
184
+ `hookSpecificOutput` envelope with `permissionDecision: "deny"`; on `allow`, blank
185
+ input, or malformed input it prints nothing and exits `0` so the host's normal
186
+ permission flow continues.
187
+
188
+ ## OpenCode setup
189
+
190
+ ```bash
191
+ bashboy install --opencode
192
+ ```
193
+
194
+ This writes two files under `~/.config/opencode` (override with `--config-dir DIR`):
195
+
196
+ - `plugins/bashboy.ts` — the generated plugin;
197
+ - `opencode.json` — registers the plugin's absolute path.
198
+
199
+ `opencode.json` is created with `{"$schema": "https://opencode.ai/config.json"}` if
200
+ absent. Existing keys and existing plugin entries are preserved, the BashBoy path
201
+ is appended only once, and the previous config is backed up before an atomic write.
202
+ OpenCode V2 documents the key as `plugins`; V1 used `plugin`. BashBoy reads
203
+ whichever key already exists, preserves it, defaults to `plugins` when neither is
204
+ present, and never writes both.
205
+
206
+ The plugin hooks both API generations with one shared helper:
207
+
208
+ - **V2:** `Plugin.define({ id: "bashboy", setup })` registering
209
+ `ctx.tool.hook("execute.before", ...)`, reading `event.input.command`.
210
+ - **V1 fallback:** `server()` returning `"tool.execute.before"`, reading
211
+ `output.args.command`.
212
+
213
+ It acts only for the `bash`/`shell` tools and runs
214
+ `uvx bashboy check --json --command <cmd> --cwd <dir>` via
215
+ `execFile(..., { shell: false, timeout: 15000, windowsHide: true })`. The command is
216
+ a single argv element; it is never passed through a shell and never concatenated
217
+ into a command line. On `deny` it throws
218
+ `Error("BashBoy blocked this command: <reason>")`. If `uvx` is missing, the exit is
219
+ non-zero without parseable JSON, the JSON is invalid, the call times out, or the
220
+ verdict is unexpected, it throws
221
+ `Error("BashBoy could not verify this command: <detail>")` — fail closed.
222
+
223
+ `bashboy install --opencode --dry-run` prints one JSON object with
224
+ `plugin_path`, `plugin_source`, and the would-be `config`, and writes nothing.
225
+
226
+ ## Escape hatch
227
+
228
+ > **`BASHBOY_DISABLE=1` disables all checks.** Commands are allowed with
229
+ > `source: "disabled"` and no denylist, allowlist, or model evaluation runs.
230
+ > For emergencies and CI debugging only. Never set it in an agent's environment
231
+ > or in a shell that an agent controls.
232
+
233
+ ```bash
234
+ BASHBOY_DISABLE=1 bashboy check --command "rm -rf /" # allow (disabled)
235
+ ```
236
+
237
+ ## Privacy
238
+
239
+ For model classification, the **command text and the working directory** are sent
240
+ to the configured Clocked endpoint over HTTPS (`text: "cwd: <cwd>\ncommand: <cmd>"`).
241
+ Nothing else is transmitted; no files are uploaded.
242
+
243
+ Fast-path verdicts (`denylist`, `allowlist`, `disabled`) never leave the machine.
244
+ There is no analytics, no audit log, no cache, and no telemetry. The API key is
245
+ read from the environment at call time, sent only as the `Authorization` header,
246
+ and never logged, persisted, or included in `config show`.
247
+
248
+ ## Known limitation: unverified Clocked envelope
249
+
250
+ The response shape of the Clocked (`clocked/fill`) endpoint is **unverified**. Parsing
251
+ is deliberately tolerant (`extract_field` walks nested objects and arrays
252
+ breadth-first, and unwraps `value`/`text`/`answer`/`label`/`result`/`content`
253
+ containers). If the response cannot be resolved to a clear `allow` or `deny`, the
254
+ verdict is `deny` with `source: "fallback"`. Report envelope mismatches with the
255
+ raw (redacted) response so the parser can be extended.
256
+
257
+ ## Limitations
258
+
259
+ - The structural allowlist is conservative by design: any operator, substitution,
260
+ redirection, or chaining disqualifies a command. False negatives simply go to the
261
+ model.
262
+ - The denylist is a fast path, not a complete policy. It catches literal destructive
263
+ patterns and payloads hidden behind `;`, `&&`, `||`, `|`, newlines, `$(...)`, and
264
+ backticks — but it does not decode base64, `r''m`-style quoting, or other
265
+ obfuscation. Such commands are not allowlisted and therefore reach the model,
266
+ which fails closed.
267
+ - PowerShell/bash parity is best-effort. The allowlist covers a small bash-oriented
268
+ set; PowerShell users should expect more commands to reach the model.
269
+ - `git reset --hard` is intentionally **not** denied: it is recoverable via the
270
+ reflog. `git clean -fdx /` at a filesystem root is denied.
271
+ - `--from SPEC` is trusted input: a user who runs `install --from <path>` is
272
+ explicitly pointing BashBoy at that checkout.
273
+ - v1 has no audit log; the host's own transcript remains the record.
274
+
275
+ ## Development
276
+
277
+ ```bash
278
+ uv run pytest # offline; no network access required
279
+ uv run bashboy selftest
280
+ ```
281
+
282
+ Layout:
283
+
284
+ ```
285
+ src/bashboy/
286
+ cli.py check / install / config / selftest / version
287
+ config.py TOML defaults, env overrides, API key lookup
288
+ rules.py structural allowlist + hard denylist (pure)
289
+ provider.py Clocked client, tolerant envelope parser
290
+ classify.py verdict pipeline (fast paths -> model -> fail closed)
291
+ hooks/claude.py Claude Code PreToolUse adapter
292
+ installers/ Claude settings.json and OpenCode plugin installers
293
+ tests/ offline test suite
294
+ ```
295
+
296
+ There are **no runtime dependencies beyond the standard library**. The provider is
297
+ imported lazily inside the model branch so the hook path stays fast, and the
298
+ installers are imported lazily by `install` only.
@@ -0,0 +1,281 @@
1
+ # BashBoy
2
+
3
+ BashBoy is a small, dependency-free Python CLI that classifies a shell command as
4
+ `allow` or `deny` **before** a coding agent runs it. It sits in front of Claude Code
5
+ (`PreToolUse` hook) and OpenCode (plugin) and answers one question: is this command
6
+ safe to run unattended?
7
+
8
+ > **BashBoy only narrows; `allow` means *no objection*.** It never approves a tool
9
+ > call. When BashBoy says `allow` it prints nothing (Claude) or returns normally
10
+ > (OpenCode), and the host's normal permission flow still applies. There is no
11
+ > `ask` verdict, no auto-approve path, and no way for BashBoy to widen permissions.
12
+
13
+ Three layers, in order:
14
+
15
+ 1. **Local fast paths** (no network): a structural allowlist for single read-only
16
+ commands, and a hard denylist for destructive ones.
17
+ 2. **Model classification** against the Clocked (`clocked/fill`) endpoint for everything
18
+ else.
19
+ 3. **Host adapters** that express denial only.
20
+
21
+ Anything the fast paths do not recognise goes to the model. Anything the model path
22
+ cannot answer clearly becomes `deny`.
23
+
24
+ ## Install
25
+
26
+ Requires Python 3.10+ and [uv](https://docs.astral.sh/uv/).
27
+
28
+ ```bash
29
+ uv tool install bashboy # install once
30
+ uvx bashboy --help # or run without installing
31
+ ```
32
+
33
+ From a local checkout (uvx resolves the project from the path):
34
+
35
+ ```bash
36
+ uvx --from /path/to/BashBoy bashboy check --command "git status"
37
+ uvx --from /path/to/BashBoy bashboy install --claude
38
+ ```
39
+
40
+ The package has **zero runtime dependencies** — everything is the Python standard
41
+ library. `pytest` is a development-only dependency.
42
+
43
+ The API key is read from the environment (default `CLOCKED_API_KEY`). It is never
44
+ written to disk:
45
+
46
+ ```bash
47
+ export CLOCKED_API_KEY=... # bash
48
+ $env:CLOCKED_API_KEY = "..." # PowerShell
49
+ ```
50
+
51
+ ## Quick start
52
+
53
+ ```bash
54
+ bashboy check --command "rm -rf /"
55
+ # deny (denylist): recursive delete of a protected path
56
+
57
+ bashboy check --command "git status"
58
+ # allow (allowlist): matches allowlist: git status
59
+
60
+ bashboy check --command "docker run --rm alpine sh" --json
61
+ # {"decision": "deny", "source": "fallback", "reason": "classifier unavailable: no API key ...", ...}
62
+ ```
63
+
64
+ Exit codes:
65
+
66
+ | Code | Meaning |
67
+ |------|---------|
68
+ | `0` | `allow` |
69
+ | `1` | `deny` |
70
+ | `2` | configuration or usage error (never a silent allow) |
71
+
72
+ `--json` prints exactly one object, in this key order:
73
+
74
+ ```json
75
+ {"decision":"deny","source":"denylist","reason":"recursive delete of a protected path","risk":null,"latency_ms":0}
76
+ ```
77
+
78
+ `source` is one of `denylist`, `allowlist`, `model`, `fallback`, `disabled`.
79
+ `--cwd DIR` supplies the working directory used for context (default: current
80
+ directory).
81
+
82
+ ## Configuration
83
+
84
+ Optional file at `~/.bashboy/config.toml` (override with `BASHBOY_CONFIG`).
85
+ Create it with `bashboy config init`; inspect with `bashboy config show`.
86
+ A missing file means these defaults:
87
+
88
+ ```toml
89
+ [provider]
90
+ endpoint = "https://clocked.rezolve.ai/clocked/fill"
91
+ api_key_env = "CLOCKED_API_KEY"
92
+ timeout_ms = 4000
93
+
94
+ [policy]
95
+ fail_mode = "closed" # closed | open
96
+ denylist = true
97
+ allowlist = true
98
+
99
+ [integrations.claude]
100
+ timeout_s = 15
101
+
102
+ [integrations.opencode]
103
+ timeout_ms = 15000
104
+ ```
105
+
106
+ Malformed TOML is a hard error (exit 2) from `check` — the gate never silently
107
+ loosens because a file failed to parse.
108
+
109
+ | Variable | Effect |
110
+ |----------|--------|
111
+ | `BASHBOY_CONFIG` | Path to the config file (default `~/.bashboy/config.toml`) |
112
+ | `BASHBOY_ENDPOINT` | Overrides `[provider].endpoint` |
113
+ | `BASHBOY_API_KEY_ENV` | Name of the environment variable holding the key |
114
+ | `BASHBOY_FAIL_MODE` | `closed` (default) or `open`; anything else is an error |
115
+ | `BASHBOY_DISABLE` | `1`/`true`/`yes` disables **all** checks (see below) |
116
+ | `BASHBOY_DEBUG` | `1`/`true`/`yes` writes diagnostics to stderr only |
117
+
118
+ ## Claude Code setup
119
+
120
+ ```bash
121
+ bashboy install --claude
122
+ ```
123
+
124
+ This merges a `PreToolUse` hook into `~/.claude/settings.json` (override with
125
+ `--settings PATH`). Unrelated keys and unrelated hook events are preserved; an
126
+ existing BashBoy handler is updated in place, so re-running never duplicates it.
127
+ The previous file is copied to `settings.json.bak` and the new file is written
128
+ atomically.
129
+
130
+ The exact entry it writes:
131
+
132
+ ```json
133
+ {
134
+ "hooks": {
135
+ "PreToolUse": [
136
+ {
137
+ "matcher": "Bash|PowerShell",
138
+ "hooks": [
139
+ {
140
+ "type": "command",
141
+ "command": "uvx",
142
+ "args": ["bashboy", "check", "--claude-hook"],
143
+ "timeout": 15,
144
+ "statusMessage": "BashBoy: checking command"
145
+ }
146
+ ]
147
+ }
148
+ ]
149
+ }
150
+ }
151
+ ```
152
+
153
+ Options:
154
+
155
+ ```bash
156
+ bashboy install --claude --dry-run # print the resulting JSON, write nothing
157
+ bashboy install --claude --settings ./settings.json
158
+ bashboy install --claude --from /path/to/BashBoy
159
+ ```
160
+
161
+ `--from SPEC` rewrites the argv to
162
+ `["--from", SPEC, "bashboy", "check", "--claude-hook"]`, so `uvx` runs the given
163
+ local checkout. The path is written verbatim as a single argv element — never
164
+ interpolated into a shell string.
165
+
166
+ The hook reads one JSON object on stdin. On `deny` it prints the
167
+ `hookSpecificOutput` envelope with `permissionDecision: "deny"`; on `allow`, blank
168
+ input, or malformed input it prints nothing and exits `0` so the host's normal
169
+ permission flow continues.
170
+
171
+ ## OpenCode setup
172
+
173
+ ```bash
174
+ bashboy install --opencode
175
+ ```
176
+
177
+ This writes two files under `~/.config/opencode` (override with `--config-dir DIR`):
178
+
179
+ - `plugins/bashboy.ts` — the generated plugin;
180
+ - `opencode.json` — registers the plugin's absolute path.
181
+
182
+ `opencode.json` is created with `{"$schema": "https://opencode.ai/config.json"}` if
183
+ absent. Existing keys and existing plugin entries are preserved, the BashBoy path
184
+ is appended only once, and the previous config is backed up before an atomic write.
185
+ OpenCode V2 documents the key as `plugins`; V1 used `plugin`. BashBoy reads
186
+ whichever key already exists, preserves it, defaults to `plugins` when neither is
187
+ present, and never writes both.
188
+
189
+ The plugin hooks both API generations with one shared helper:
190
+
191
+ - **V2:** `Plugin.define({ id: "bashboy", setup })` registering
192
+ `ctx.tool.hook("execute.before", ...)`, reading `event.input.command`.
193
+ - **V1 fallback:** `server()` returning `"tool.execute.before"`, reading
194
+ `output.args.command`.
195
+
196
+ It acts only for the `bash`/`shell` tools and runs
197
+ `uvx bashboy check --json --command <cmd> --cwd <dir>` via
198
+ `execFile(..., { shell: false, timeout: 15000, windowsHide: true })`. The command is
199
+ a single argv element; it is never passed through a shell and never concatenated
200
+ into a command line. On `deny` it throws
201
+ `Error("BashBoy blocked this command: <reason>")`. If `uvx` is missing, the exit is
202
+ non-zero without parseable JSON, the JSON is invalid, the call times out, or the
203
+ verdict is unexpected, it throws
204
+ `Error("BashBoy could not verify this command: <detail>")` — fail closed.
205
+
206
+ `bashboy install --opencode --dry-run` prints one JSON object with
207
+ `plugin_path`, `plugin_source`, and the would-be `config`, and writes nothing.
208
+
209
+ ## Escape hatch
210
+
211
+ > **`BASHBOY_DISABLE=1` disables all checks.** Commands are allowed with
212
+ > `source: "disabled"` and no denylist, allowlist, or model evaluation runs.
213
+ > For emergencies and CI debugging only. Never set it in an agent's environment
214
+ > or in a shell that an agent controls.
215
+
216
+ ```bash
217
+ BASHBOY_DISABLE=1 bashboy check --command "rm -rf /" # allow (disabled)
218
+ ```
219
+
220
+ ## Privacy
221
+
222
+ For model classification, the **command text and the working directory** are sent
223
+ to the configured Clocked endpoint over HTTPS (`text: "cwd: <cwd>\ncommand: <cmd>"`).
224
+ Nothing else is transmitted; no files are uploaded.
225
+
226
+ Fast-path verdicts (`denylist`, `allowlist`, `disabled`) never leave the machine.
227
+ There is no analytics, no audit log, no cache, and no telemetry. The API key is
228
+ read from the environment at call time, sent only as the `Authorization` header,
229
+ and never logged, persisted, or included in `config show`.
230
+
231
+ ## Known limitation: unverified Clocked envelope
232
+
233
+ The response shape of the Clocked (`clocked/fill`) endpoint is **unverified**. Parsing
234
+ is deliberately tolerant (`extract_field` walks nested objects and arrays
235
+ breadth-first, and unwraps `value`/`text`/`answer`/`label`/`result`/`content`
236
+ containers). If the response cannot be resolved to a clear `allow` or `deny`, the
237
+ verdict is `deny` with `source: "fallback"`. Report envelope mismatches with the
238
+ raw (redacted) response so the parser can be extended.
239
+
240
+ ## Limitations
241
+
242
+ - The structural allowlist is conservative by design: any operator, substitution,
243
+ redirection, or chaining disqualifies a command. False negatives simply go to the
244
+ model.
245
+ - The denylist is a fast path, not a complete policy. It catches literal destructive
246
+ patterns and payloads hidden behind `;`, `&&`, `||`, `|`, newlines, `$(...)`, and
247
+ backticks — but it does not decode base64, `r''m`-style quoting, or other
248
+ obfuscation. Such commands are not allowlisted and therefore reach the model,
249
+ which fails closed.
250
+ - PowerShell/bash parity is best-effort. The allowlist covers a small bash-oriented
251
+ set; PowerShell users should expect more commands to reach the model.
252
+ - `git reset --hard` is intentionally **not** denied: it is recoverable via the
253
+ reflog. `git clean -fdx /` at a filesystem root is denied.
254
+ - `--from SPEC` is trusted input: a user who runs `install --from <path>` is
255
+ explicitly pointing BashBoy at that checkout.
256
+ - v1 has no audit log; the host's own transcript remains the record.
257
+
258
+ ## Development
259
+
260
+ ```bash
261
+ uv run pytest # offline; no network access required
262
+ uv run bashboy selftest
263
+ ```
264
+
265
+ Layout:
266
+
267
+ ```
268
+ src/bashboy/
269
+ cli.py check / install / config / selftest / version
270
+ config.py TOML defaults, env overrides, API key lookup
271
+ rules.py structural allowlist + hard denylist (pure)
272
+ provider.py Clocked client, tolerant envelope parser
273
+ classify.py verdict pipeline (fast paths -> model -> fail closed)
274
+ hooks/claude.py Claude Code PreToolUse adapter
275
+ installers/ Claude settings.json and OpenCode plugin installers
276
+ tests/ offline test suite
277
+ ```
278
+
279
+ There are **no runtime dependencies beyond the standard library**. The provider is
280
+ imported lazily inside the model branch so the hook path stays fast, and the
281
+ installers are imported lazily by `install` only.
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "bashboy"
7
+ version = "0.1.0"
8
+ description = "Classify shell commands as allow or deny before an agent runs them."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ dependencies = []
12
+ license = "MIT"
13
+ license-files = ["LICENSE"]
14
+ authors = [{ name = "Mann10", email = "82712171+Mann10@users.noreply.github.com" }]
15
+ keywords = ["security", "agents", "claude-code", "opencode", "shell", "guardrails"]
16
+ classifiers = [
17
+ "Programming Language :: Python :: 3",
18
+ "Operating System :: OS Independent",
19
+ "Topic :: Security",
20
+ ]
21
+
22
+ [project.urls]
23
+ Homepage = "https://github.com/Mann10/bashboy"
24
+ Issues = "https://github.com/Mann10/bashboy/issues"
25
+ Source = "https://github.com/Mann10/bashboy"
26
+
27
+ [project.scripts]
28
+ bashboy = "bashboy.cli:main"
29
+
30
+ [dependency-groups]
31
+ dev = ["pytest>=8"]
32
+
33
+ [tool.hatch.build.targets.wheel]
34
+ packages = ["src/bashboy"]
35
+
36
+ [tool.hatch.build.targets.sdist]
37
+ exclude = [".planning", "dist", ".venv", ".pytest_cache"]
38
+
39
+ [tool.pytest.ini_options]
40
+ testpaths = ["tests"]
41
+ addopts = "-q"
@@ -0,0 +1,5 @@
1
+ """BashBoy: a deny-only shell-command gate for coding agents."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ __all__ = ["__version__"]