nofax 0.2.0

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tomi Šeregi
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.
package/README.md ADDED
@@ -0,0 +1,351 @@
1
+ # Nofax
2
+
3
+ [![CI](https://github.com/AKzar1el/nofax/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/AKzar1el/nofax/actions/workflows/ci.yml)
4
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
+ [![Node.js >=20](https://img.shields.io/badge/node-%3E%3D20-339933?logo=node.js&logoColor=white)](package.json)
6
+ [![MCP](https://img.shields.io/badge/MCP-compatible-6f42c1)](https://modelcontextprotocol.io/)
7
+
8
+ **Human-in-the-loop approvals and notifications for AI coding agents, without running a Nofax SaaS.**
9
+
10
+ Nofax is a small open-source bridge between an agent and a human. Local mode can pause an AI workflow, notify your phone, and return an explicit decision. An optional self-deployed Cloudflare Worker exposes a deliberately narrower remote MCP surface for one-way notifications and safe request inspection.
11
+
12
+ No Nofax account. No paid model API. No inbound port on your machine. MIT licensed.
13
+
14
+ > **Current status:** local Nofax is `0.2.0`. The optional Cloudflare Worker is the upcoming `0.3.0` remote surface and is developed alongside the local package.
15
+
16
+ ## Why Nofax
17
+
18
+ Agent workflows increasingly need a clean answer to one question: **when automation reaches a human decision boundary, how does it ask without pretending that silence means approval?**
19
+
20
+ Nofax keeps that boundary explicit:
21
+
22
+ - pending is never approval;
23
+ - timeout and transport failure fail closed;
24
+ - the first accepted terminal response wins;
25
+ - agent-specific hook schemas stay isolated in adapters;
26
+ - remote access is intentionally narrower than local access;
27
+ - Nofax does not grant authority the calling agent did not already have.
28
+
29
+ ## Two operating modes
30
+
31
+ | Capability | Local Nofax `0.2` | Remote Worker `0.3` |
32
+ | --- | --- | --- |
33
+ | Transport | stdio / CLI hooks | MCP Streamable HTTP |
34
+ | One-way notification | Yes | Yes |
35
+ | Allow / Deny | Yes | No |
36
+ | Explicit choices | Yes | No |
37
+ | Free-text refinement | Yes | No |
38
+ | Wait for human response | Yes | No |
39
+ | Read request metadata | Yes | Yes |
40
+ | Durable state | Local files | Existing SQLite Durable Object rows |
41
+ | Hosted by Nofax | No | No — self-deployed Worker |
42
+ | Remote authentication | Local process boundary | Private bearer key |
43
+
44
+ The remote Worker is **not** a hosted remote-approval service. It can send an informational notification and inspect existing request state, but it has no approval callback, choice, refinement, wait, webhook, or arbitrary remote-write endpoint.
45
+
46
+ ## Quick start
47
+
48
+ ### 1. Install
49
+
50
+ Until a registry release is published:
51
+
52
+ ```bash
53
+ npm install -g https://github.com/AKzar1el/nofax.git
54
+ ```
55
+
56
+ Requires Node.js 20 or newer.
57
+
58
+ ### 2. Initialize
59
+
60
+ ```bash
61
+ nofax init
62
+ ```
63
+
64
+ Nofax creates `~/.nofax/config.json` and generates a high-entropy notification topic. With the default transport, subscribe to the displayed topic in the ntfy mobile app.
65
+
66
+ ### 3. Test
67
+
68
+ ```bash
69
+ nofax test
70
+ ```
71
+
72
+ ### 4. Use it
73
+
74
+ ```bash
75
+ nofax notify --title "Build finished" "All tests passed"
76
+ nofax approve --title "Deploy?" "Release 1.4.0 is ready"
77
+ nofax refine --title "Refine draft" "Tell me what to change"
78
+ ```
79
+
80
+ An approval resolves to stable terminal JSON:
81
+
82
+ ```json
83
+ {"decision":"allow"}
84
+ ```
85
+
86
+ or:
87
+
88
+ ```json
89
+ {"decision":"deny"}
90
+ ```
91
+
92
+ If the request is still pending, times out, disconnects, or hits a transport error, Nofax never converts that condition into approval.
93
+
94
+ ## MCP
95
+
96
+ Start the local stdio MCP server:
97
+
98
+ ```bash
99
+ nofax mcp
100
+ ```
101
+
102
+ Local MCP exposes:
103
+
104
+ - `nofax_notify`
105
+ - `nofax_request_approval`
106
+ - `nofax_request_choice`
107
+ - `nofax_request_refinement`
108
+ - `nofax_wait_for_response`
109
+ - `nofax_get_request`
110
+ - `nofax_list_pending`
111
+
112
+ Interactive requests return a durable request ID. `nofax_wait_for_response` performs a bounded wait; callers must repeat the wait while the request remains pending rather than infer approval.
113
+
114
+ ## Agent integrations
115
+
116
+ ### Claude Code
117
+
118
+ Use Nofax as a local `PermissionRequest` hook in `~/.claude/settings.json`:
119
+
120
+ ```json
121
+ {
122
+ "hooks": {
123
+ "PermissionRequest": [
124
+ {
125
+ "matcher": ".*",
126
+ "hooks": [
127
+ {
128
+ "type": "command",
129
+ "command": "nofax hook claude"
130
+ }
131
+ ]
132
+ }
133
+ ]
134
+ }
135
+ }
136
+ ```
137
+
138
+ ### Codex
139
+
140
+ Codex hooks are enabled by default. Configure `~/.codex/hooks.json`:
141
+
142
+ ```json
143
+ {
144
+ "hooks": {
145
+ "PermissionRequest": [
146
+ {
147
+ "matcher": ".*",
148
+ "hooks": [
149
+ {
150
+ "type": "command",
151
+ "command": "nofax hook codex",
152
+ "statusMessage": "Waiting for Nofax approval"
153
+ }
154
+ ]
155
+ }
156
+ ]
157
+ }
158
+ }
159
+ ```
160
+
161
+ Restart Codex, run `/hooks`, and review/trust the exact Nofax hook definition before relying on it. Codex skips non-managed hooks until they are trusted, and a changed hook definition must be reviewed again. If an administrator or local policy has explicitly disabled hooks, re-enable them with `[features] hooks = true` in `~/.codex/config.toml`.
162
+
163
+ ### Gemini CLI
164
+
165
+ Current Gemini CLI builds expose a synchronous `BeforeTool` hook that can allow or deny a tool call. Route selected tools through Nofax in `~/.gemini/settings.json`:
166
+
167
+ ```json
168
+ {
169
+ "hooks": {
170
+ "BeforeTool": [
171
+ {
172
+ "matcher": "run_shell_command|write_file|replace",
173
+ "hooks": [
174
+ {
175
+ "name": "nofax-approval",
176
+ "type": "command",
177
+ "command": "nofax hook gemini",
178
+ "timeout": 305000
179
+ }
180
+ ]
181
+ }
182
+ ],
183
+ "Notification": [
184
+ {
185
+ "matcher": "ToolPermission",
186
+ "hooks": [
187
+ {
188
+ "name": "nofax-notification",
189
+ "type": "command",
190
+ "command": "nofax hook gemini"
191
+ }
192
+ ]
193
+ }
194
+ ]
195
+ }
196
+ }
197
+ ```
198
+
199
+ `BeforeTool` waits for an explicit Nofax Allow/Deny result. A Nofax timeout or transport failure emits valid no-decision JSON and leaves Gemini CLI's own policy/confirmation flow in control rather than converting failure into approval. The `Notification` hook remains advisory and is forwarded only as a phone notification.
200
+
201
+ Adjust the matcher to the tools you want Nofax to gate. Keep the hook timeout longer than Nofax's configured approval timeout (`timeoutSeconds`, 300 seconds by default).
202
+
203
+ ## Optional remote Cloudflare Worker
204
+
205
+ The `worker/` package provides a private, self-deployed MCP endpoint:
206
+
207
+ ```text
208
+ remote MCP client
209
+ |
210
+ | authenticated Streamable HTTP
211
+ v
212
+ Cloudflare Worker
213
+ |
214
+ +--> nofax_notify ------> ntfy ------> phone
215
+ |
216
+ +--> SQLite Durable Object
217
+ |
218
+ +--> get request metadata
219
+ +--> list pending requests
220
+ ```
221
+
222
+ It exposes exactly three tools:
223
+
224
+ - `nofax_notify` — one-way notification only;
225
+ - `nofax_get_request` — read one safe request projection;
226
+ - `nofax_list_pending` — read unresolved, unexpired request projections.
227
+
228
+ Deploy from `worker/`:
229
+
230
+ ```bash
231
+ npm ci
232
+ npx wrangler login
233
+ npx wrangler secret put NOFAX_REMOTE_KEY
234
+ npx wrangler secret put NTFY_TOPIC
235
+ npm run check
236
+ npm run deploy
237
+ ```
238
+
239
+ Preferred MCP connection:
240
+
241
+ ```text
242
+ https://<worker>.workers.dev/mcp
243
+ Authorization: Bearer <NOFAX_REMOTE_KEY>
244
+ ```
245
+
246
+ Clients that cannot attach a static authorization header can use the compatibility capability path:
247
+
248
+ ```text
249
+ https://<worker>.workers.dev/mcp/<NOFAX_REMOTE_KEY>
250
+ ```
251
+
252
+ Treat the complete capability URL like a password.
253
+
254
+ See [`docs/remote-mcp.md`](docs/remote-mcp.md) for deployment, threat boundaries, and qualification details.
255
+
256
+ ### Important: public ntfy + serverless egress
257
+
258
+ The default public `ntfy.sh` service applies publisher quotas. Serverless platforms such as Cloudflare Workers may use shared outbound IP space, so a Worker can receive an ntfy `42908` daily-quota response even when that individual Worker has sent very little traffic. That limit is imposed by ntfy, not by the Cloudflare Workers request quota.
259
+
260
+ For reliability-sensitive deployments, use a notification provider whose quota is tied to your own authenticated account/identity, or operate a trusted self-hosted transport. Do not build a critical workflow around anonymous public-topic quota assumptions.
261
+
262
+ ## Security model
263
+
264
+ Nofax is a transport and human-interaction component, **not an authorization policy engine**.
265
+
266
+ Local mode:
267
+
268
+ - pending, timeout, disconnect, malformed state, and network failure never mean approval;
269
+ - the first valid terminal response wins;
270
+ - notification topics and one-time response topics are capabilities;
271
+ - public ntfy is not end-to-end encrypted from the provider;
272
+ - redaction is best-effort and cannot reliably identify secrets embedded in arbitrary free-form text.
273
+
274
+ Remote mode:
275
+
276
+ - only explicit `nofax_notify` performs an external messaging side effect;
277
+ - request-inspection operations are read-only and do not perform hidden cleanup writes;
278
+ - remote approval, callback, webhook, refinement, choice, and wait surfaces are absent;
279
+ - `NOFAX_REMOTE_KEY` is a bearer credential;
280
+ - remote projections omit callback capabilities, prompt/message text, and internal allowed-decision lists.
281
+
282
+ Read [`SECURITY.md`](SECURITY.md) before using Nofax with sensitive information.
283
+
284
+ ## Configuration
285
+
286
+ Default local config lives at `~/.nofax/config.json`:
287
+
288
+ ```json
289
+ {
290
+ "version": 1,
291
+ "server": "https://ntfy.sh",
292
+ "topic": "nofax_<random>",
293
+ "timeoutSeconds": 300
294
+ }
295
+ ```
296
+
297
+ Override the home directory with `NOFAX_HOME`:
298
+
299
+ ```bash
300
+ NOFAX_HOME=/path/to/nofax-home nofax config
301
+ ```
302
+
303
+ Use another ntfy-compatible server with:
304
+
305
+ ```bash
306
+ nofax init --server https://ntfy.example.com --force
307
+ ```
308
+
309
+ ## Development
310
+
311
+ Local package:
312
+
313
+ ```bash
314
+ npm ci
315
+ npm run check
316
+ npm test
317
+ npm pack --dry-run
318
+ ```
319
+
320
+ Remote Worker:
321
+
322
+ ```bash
323
+ cd worker
324
+ npm ci
325
+ npm run check
326
+ ```
327
+
328
+ CI qualifies Node.js 20, 22, and 24 for the local package. The Worker gate runs TypeScript, Vitest, a production-dependency audit, and a Wrangler deployment dry-run.
329
+
330
+ ## Project docs
331
+
332
+ - [`docs/architecture.md`](docs/architecture.md) — trust boundaries and data flow
333
+ - [`docs/remote-mcp.md`](docs/remote-mcp.md) — remote Worker deployment and qualification
334
+ - [`SECURITY.md`](SECURITY.md) — security assumptions and vulnerability reporting
335
+ - [`CONTRIBUTING.md`](CONTRIBUTING.md) — contribution and test expectations
336
+ - [`CHANGELOG.md`](CHANGELOG.md) — release history
337
+
338
+ ## Non-goals
339
+
340
+ Nofax deliberately does not provide:
341
+
342
+ - a Nofax-operated approval SaaS;
343
+ - a paid model API dependency;
344
+ - persistent `always approve` policy;
345
+ - an arbitrary remote shell endpoint;
346
+ - a public multi-user Worker behind one shared deployment key;
347
+ - a claim that MCP annotations themselves are a security boundary.
348
+
349
+ ## License
350
+
351
+ MIT © Tomi Šeregi. See [`LICENSE`](LICENSE).
package/SECURITY.md ADDED
@@ -0,0 +1,144 @@
1
+ # Security Policy
2
+
3
+ ## Supported versions
4
+
5
+ Nofax is pre-1.0 software. Security fixes are applied to the latest release on the default branch.
6
+
7
+ ## Reporting a vulnerability
8
+
9
+ Do not publish credentials, exploit details, private ntfy topics, one-time response URLs, remote MCP capability URLs, or sensitive hook/MCP payloads in a public issue.
10
+
11
+ If GitHub private vulnerability reporting is enabled for this repository, use it. Otherwise, open a minimal public issue asking for a private maintainer contact channel without including sensitive details.
12
+
13
+ ## Local Nofax security
14
+
15
+ ### Public ntfy is not end-to-end encryption
16
+
17
+ With the default `https://ntfy.sh` configuration, notification content transits and may be cached by the ntfy service. A random topic name reduces unauthorized discovery but does not encrypt content from the service operator.
18
+
19
+ Use a trusted authenticated/self-hosted ntfy server for sensitive source code, production operations, credentials, regulated data, or confidential prompts.
20
+
21
+ ### Topics and one-time response URLs are capabilities
22
+
23
+ On anonymous ntfy servers, knowledge of a topic can be sufficient to subscribe or publish. Treat the local ntfy topic as a bearer secret.
24
+
25
+ Local Nofax generates fresh one-time response topics for interactive requests. Do not log, share, bookmark, or persist those callback URLs outside the state Nofax itself requires.
26
+
27
+ ### Durable local request state
28
+
29
+ Local MCP human-response requests are stored under `~/.nofax/requests/` so an MCP/client restart does not erase an unresolved human decision gate.
30
+
31
+ Nofax writes these files with user-only permissions where supported. MCP-facing request projections deliberately omit the secret response topic. Protect the Nofax home directory like other local application state.
32
+
33
+ ### Pending never means approved
34
+
35
+ A local interactive request remains pending until a matching terminal response is accepted. Passage of time, timeout, tool failure, network failure, or client disconnect never means approval.
36
+
37
+ `nofax_wait_for_response` uses bounded long-polls. If it returns pending, the caller must wait again rather than continue the guarded action.
38
+
39
+ ### First terminal response wins
40
+
41
+ Local durable requests are single-use. Once a valid terminal response is accepted, later responses cannot intentionally replace it.
42
+
43
+ ### Native hook failures never become approval
44
+
45
+ The Claude Code and Codex adapters fail closed. If the local transport times out, returns malformed data, or encounters a network/polling error, Nofax does not synthesize an Allow decision.
46
+
47
+ ### Redaction is best-effort
48
+
49
+ Nofax redacts values under common secret-bearing object keys and bounds serialized payloads. It cannot reliably detect a credential embedded in arbitrary free-form command text. Treat all notification content accordingly.
50
+
51
+ ## Remote Worker security
52
+
53
+ The optional Cloudflare Worker in v0.3 is a **one-way notification plus inspection remote MCP endpoint**.
54
+
55
+ ### Remote side effects are structurally bounded
56
+
57
+ MCP tool annotations are descriptive metadata, not the security boundary.
58
+
59
+ The remote Worker bounds side effects in code:
60
+
61
+ - only `nofax_notify`, `nofax_get_request`, and `nofax_list_pending` are registered as MCP tools;
62
+ - the remote handler object exposes one-way notification plus two read operations;
63
+ - there is no remote approval, choice, refinement, wait, callback, or webhook handler;
64
+ - public Worker routing is limited to health and authenticated MCP paths;
65
+ - `/telegram/webhook` and `/r/*` are not routes and return 404;
66
+ - pending-list reads filter expired rows without deleting them or performing hidden cleanup writes.
67
+
68
+ The two inspection tools are annotated with `readOnlyHint: true`, `destructiveHint: false`, `idempotentHint: true`, and `openWorldHint: false`. `nofax_notify` is accurately marked side-effecting/non-idempotent/open-world and non-destructive.
69
+
70
+ ### Remote response minimization
71
+
72
+ Remote MCP responses deliberately omit capability-bearing or unnecessary fields. Public request projections do not expose:
73
+
74
+ - callback hashes;
75
+ - callback tokens/URLs;
76
+ - original request title;
77
+ - original request message/prompt material;
78
+ - internal allowed-decision lists.
79
+
80
+ Terminal state may include the stored terminal decision and historical refinement text when present in existing durable state.
81
+
82
+ ### Remote MCP key is a bearer secret
83
+
84
+ The Worker is a private single-user deployment protected by `NOFAX_REMOTE_KEY`.
85
+
86
+ Preferred clients send:
87
+
88
+ ```text
89
+ Authorization: Bearer <NOFAX_REMOTE_KEY>
90
+ ```
91
+
92
+ Clients that cannot attach a static header may use:
93
+
94
+ ```text
95
+ /mcp/<NOFAX_REMOTE_KEY>
96
+ ```
97
+
98
+ The complete capability URL is equivalent to a password and may leak through browser history, screenshots, copied configuration, proxies, or third-party logging. Prefer the Authorization header whenever the MCP host supports it.
99
+
100
+ Nofax uses equal-length constant-time credential comparison. After capability-path authentication, the Worker normalizes the request internally to `/mcp` before MCP protocol handling.
101
+
102
+ Rotate `NOFAX_REMOTE_KEY` immediately if it is exposed.
103
+
104
+ A single deployment-wide key is not sufficient for a shared/public multi-user service. Use a delegated authentication and authorization design before operating a multi-user deployment.
105
+
106
+ ### Durable Object compatibility
107
+
108
+ The v0.3 Worker preserves the existing SQLite request schema so upgrading from experimental pre-read-only Worker builds does not require destructive storage migration.
109
+
110
+ Only request-inspection methods are reachable for Durable Object state. Existing legacy rows may therefore be inspected after upgrade, but the Worker cannot create, resolve, or delete them through MCP or HTTP routes. The separate `nofax_notify` method can publish an explicitly requested one-way notification and does not mutate request state.
111
+
112
+ ### Cloudflare is the remote trust boundary
113
+
114
+ Remote mode adds Cloudflare as an infrastructure boundary. For notification calls, ntfy is an additional provider boundary and receives the bounded notification title/message plus the configured topic identifier.
115
+
116
+ The Worker does not automatically forward durable request records to ntfy. Only explicit `nofax_notify` content is published; Telegram, WhatsApp, SMS, and human-response callbacks remain absent remotely.
117
+
118
+ Do not expose the deployment key in source, Wrangler vars, `.env`, `.dev.vars`, CI logs, PR text, screenshots, or issue reports. Use Wrangler secrets for production values and keep local secret files untracked.
119
+
120
+ ### Hosted ntfy quotas are an external availability boundary
121
+
122
+ The public `ntfy.sh` service applies its own publisher limits independently of Cloudflare Workers quotas. Serverless egress may be shared between unrelated workloads, so a Cloudflare Worker can receive an ntfy `42908` daily-quota response even when that individual Worker has published little traffic.
123
+
124
+ Treat public ntfy availability and quota policy as an external dependency. Reliability-sensitive deployments should use a transport with account-scoped quota/identity or a trusted self-hosted service rather than assuming anonymous public-topic capacity.
125
+
126
+ ## Nofax is not a policy engine
127
+
128
+ Local Nofax answers approval requests that an upstream agent or workflow explicitly delegates to it. It does not decide which operations should require approval and must not be used to bypass an agent's deny rules, sandbox, or existing authorization boundaries.
129
+
130
+ An `allow` result permits the caller to continue only within authority it already possessed.
131
+
132
+ The remote Worker does not grant authority. Its only mutation is sending an informational notification; request-state operations remain inspection-only.
133
+
134
+ ## MCP transport notes
135
+
136
+ ### Local stdio
137
+
138
+ `nofax mcp` is a local stdio server. Stdout is reserved for MCP protocol traffic. Do not wrap it with tooling that injects banners or diagnostics into stdout.
139
+
140
+ ### Remote Streamable HTTP
141
+
142
+ The optional Worker uses authenticated Streamable HTTP for MCP. The only unauthenticated functional route is the secret-free `/healthz` health check.
143
+
144
+ See [`docs/remote-mcp.md`](docs/remote-mcp.md) for the remote architecture and qualification checklist.
package/bin/nofax.mjs ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { runCli } from '../src/cli.mjs';
3
+
4
+ process.exitCode = await runCli(process.argv.slice(2));
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "nofax",
3
+ "version": "0.2.0",
4
+ "mcpName": "io.github.AKzar1el/nofax",
5
+ "description": "Human-in-the-loop approvals and notifications for AI coding agents via CLI, MCP, and agent hooks.",
6
+ "type": "module",
7
+ "bin": {
8
+ "nofax": "./bin/nofax.mjs"
9
+ },
10
+ "exports": {
11
+ ".": "./src/index.mjs",
12
+ "./mcp": "./src/mcp-server.mjs"
13
+ },
14
+ "files": [
15
+ "bin/",
16
+ "src/",
17
+ "README.md",
18
+ "LICENSE",
19
+ "SECURITY.md"
20
+ ],
21
+ "scripts": {
22
+ "test": "node --test test/*.test.mjs",
23
+ "check": "node --check bin/nofax.mjs && node --check src/cli.mjs && node --check src/config.mjs && node --check src/protocol.mjs && node --check src/ntfy.mjs && node --check src/requests.mjs && node --check src/mcp-tools.mjs && node --check src/mcp-server.mjs && node --check src/adapters/claude.mjs && node --check src/adapters/codex.mjs && node --check src/adapters/gemini.mjs"
24
+ },
25
+ "engines": {
26
+ "node": ">=20.0.0"
27
+ },
28
+ "dependencies": {
29
+ "@modelcontextprotocol/server": "2.0.0",
30
+ "zod": "4.4.3"
31
+ },
32
+ "keywords": [
33
+ "ai-agent",
34
+ "approval",
35
+ "claude-code",
36
+ "codex",
37
+ "gemini-cli",
38
+ "human-in-the-loop",
39
+ "mcp",
40
+ "model-context-protocol",
41
+ "ntfy",
42
+ "notifications",
43
+ "remote",
44
+ "developer-tools"
45
+ ],
46
+ "license": "MIT",
47
+ "author": "Tomi Šeregi",
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "git+https://github.com/AKzar1el/nofax.git"
51
+ },
52
+ "homepage": "https://github.com/AKzar1el/nofax#readme",
53
+ "bugs": {
54
+ "url": "https://github.com/AKzar1el/nofax/issues"
55
+ }
56
+ }
@@ -0,0 +1,47 @@
1
+ import { buildAgentSummary } from '../protocol.mjs';
2
+ import { requestApproval } from '../ntfy.mjs';
3
+
4
+ function assertClaudePermissionRequest(input) {
5
+ if (!input || input.hook_event_name !== 'PermissionRequest') throw new Error('NOFAX_CLAUDE_EVENT');
6
+ if (typeof input.tool_name !== 'string' || !input.tool_name) throw new Error('NOFAX_CLAUDE_TOOL');
7
+ }
8
+
9
+ export async function handleClaudePermissionRequest(input, {
10
+ config,
11
+ requestApprovalImpl = requestApproval,
12
+ onError = () => {}
13
+ } = {}) {
14
+ assertClaudePermissionRequest(input);
15
+ try {
16
+ const result = await requestApprovalImpl({
17
+ config,
18
+ title: `Claude Code needs approval: ${input.tool_name}`,
19
+ message: buildAgentSummary({
20
+ source: 'Claude Code',
21
+ toolName: input.tool_name,
22
+ cwd: input.cwd,
23
+ toolInput: input.tool_input
24
+ })
25
+ });
26
+ if (result.decision === 'allow') {
27
+ return {
28
+ hookSpecificOutput: {
29
+ hookEventName: 'PermissionRequest',
30
+ decision: { behavior: 'allow' }
31
+ }
32
+ };
33
+ }
34
+ if (result.decision === 'deny') {
35
+ return {
36
+ hookSpecificOutput: {
37
+ hookEventName: 'PermissionRequest',
38
+ decision: { behavior: 'deny', message: 'Denied remotely via Nofax.' }
39
+ }
40
+ };
41
+ }
42
+ return null;
43
+ } catch (error) {
44
+ onError(error);
45
+ return null;
46
+ }
47
+ }
@@ -0,0 +1,47 @@
1
+ import { buildAgentSummary } from '../protocol.mjs';
2
+ import { requestApproval } from '../ntfy.mjs';
3
+
4
+ function assertCodexPermissionRequest(input) {
5
+ if (!input || input.hook_event_name !== 'PermissionRequest') throw new Error('NOFAX_CODEX_EVENT');
6
+ if (typeof input.tool_name !== 'string' || !input.tool_name) throw new Error('NOFAX_CODEX_TOOL');
7
+ }
8
+
9
+ export async function handleCodexPermissionRequest(input, {
10
+ config,
11
+ requestApprovalImpl = requestApproval,
12
+ onError = () => {}
13
+ } = {}) {
14
+ assertCodexPermissionRequest(input);
15
+ try {
16
+ const result = await requestApprovalImpl({
17
+ config,
18
+ title: `Codex needs approval: ${input.tool_name}`,
19
+ message: buildAgentSummary({
20
+ source: 'Codex',
21
+ toolName: input.tool_name,
22
+ cwd: input.cwd,
23
+ toolInput: input.tool_input
24
+ })
25
+ });
26
+ if (result.decision === 'allow') {
27
+ return {
28
+ hookSpecificOutput: {
29
+ hookEventName: 'PermissionRequest',
30
+ decision: { behavior: 'allow' }
31
+ }
32
+ };
33
+ }
34
+ if (result.decision === 'deny') {
35
+ return {
36
+ hookSpecificOutput: {
37
+ hookEventName: 'PermissionRequest',
38
+ decision: { behavior: 'deny', message: 'Denied remotely via Nofax.' }
39
+ }
40
+ };
41
+ }
42
+ return null;
43
+ } catch (error) {
44
+ onError(error);
45
+ return null;
46
+ }
47
+ }