killm 1.0.0 → 1.0.3

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/README.md CHANGED
@@ -39,6 +39,36 @@ Editing the hosts file needs elevated privileges:
39
39
  - **macOS / Linux:** prefix with `sudo` → `sudo npx killm for 1h --agents`
40
40
  - **Windows:** run from an **Administrator** terminal
41
41
 
42
+ ### WSL users — read this
43
+
44
+ WSL and Windows have **separate hosts files**. Running `sudo npx killm` inside
45
+ WSL edits WSL's `/etc/hosts`, which only blocks processes running _inside_ WSL
46
+ (curl, node, coding agents in your WSL shell). Your browser is a Windows app —
47
+ it reads `C:\Windows\System32\drivers\etc\hosts` and is **not affected**.
48
+
49
+ To block Windows apps (the browser, Windows-side editors), run killm **on
50
+ Windows** from an Administrator PowerShell:
51
+
52
+ ```powershell
53
+ npx killm for 1h --web
54
+ ```
55
+
56
+ killm detects WSL and prints a warning when this applies. If you want both
57
+ sides blocked, run it in both places.
58
+
59
+ ### Browser caveats
60
+
61
+ Two browser behaviors can make a block look like it isn't working:
62
+
63
+ - **Secure DNS (DNS-over-HTTPS):** when enabled, the browser resolves names
64
+ through an encrypted remote resolver and **bypasses the hosts file
65
+ entirely**. Disable it (Chrome/Edge: Settings → Privacy → Security → "Use
66
+ secure DNS"; Firefox: Settings → Privacy → DNS over HTTPS) for the block to
67
+ apply.
68
+ - **Browser DNS caching:** already-open tabs and cached lookups keep working
69
+ for a while. Restart the browser, or clear its DNS cache
70
+ (`chrome://net-internals/#dns` in Chrome/Edge).
71
+
42
72
  ## The key idea: agents vs. web
43
73
 
44
74
  The whole point of `killm` is that an agentic coding tool and a chat website
@@ -95,6 +125,7 @@ You can combine `--agents` and `--web`; that's the same as `--all`.
95
125
 
96
126
  | Option | Effect |
97
127
  | ----------------- | ------------------------------------------------ |
128
+ | `--firewall` | Also block current IPs at the OS firewall |
98
129
  | `--restore` | Lift any active block right now and exit |
99
130
  | `--status` | Report whether a block is currently active |
100
131
  | `--list` | Print the hostnames a given scope would block |
@@ -132,12 +163,55 @@ target hostname at `0.0.0.0` (and `::1` for IPv6):
132
163
  ```
133
164
 
134
165
  It then flushes the OS DNS cache so the change takes effect immediately. When
135
- the timer expires, or on `Ctrl+C`, `SIGTERM`, or process exit, it strips that
136
- block back out and flushes again. The markers mean it only ever touches its own
137
- lines — your existing hosts entries are left alone.
166
+ the timer expires or on `Ctrl+C`, `SIGTERM`, or closing the terminal
167
+ (`SIGHUP`) — it strips that block back out and flushes again. The markers mean
168
+ it only ever touches its own lines — your existing hosts entries are left
169
+ alone.
170
+
171
+ ### If the killm process dies, the block stays — until killm heals it
172
+
173
+ The timer lives in the killm process, so **keep it running** for the duration.
174
+ Ctrl+C and closing the terminal both lift the block cleanly, but a `kill -9`,
175
+ a crash, or a machine shutdown can strand the block in your hosts file.
176
+
177
+ killm writes the expiry time into the block itself, so it self-heals: **any
178
+ later killm command** (`--status`, `--restore`, or starting a new block)
179
+ notices an expired stranded block and removes it. To lift one immediately:
180
+
181
+ ```bash
182
+ sudo npx killm --restore
183
+ ```
184
+
185
+ `killm --status` also tells you when an active block is scheduled to lift.
186
+
187
+ ## Firewall mode (`--firewall`)
188
+
189
+ The hosts file only intercepts name resolution, so a browser with **Secure DNS
190
+ (DoH)** enabled bypasses it. `--firewall` closes that hole: in addition to the
191
+ hosts entries, killm resolves each target hostname to its **current IPs** and
192
+ blocks those at the OS firewall:
193
+
194
+ ```bash
195
+ sudo npx killm for 1h --web --firewall
196
+ ```
197
+
198
+ - **Linux:** `iptables` / `ip6tables` OUTPUT rules, tagged with a `killm`
199
+ comment
200
+ - **Windows:** one outbound Windows Firewall rule named `killm` (via `netsh`)
201
+ - **macOS:** a `pf` anchor named `killm`
202
+
203
+ Rules are removed together with the hosts block (timer, `Ctrl+C`, or
204
+ `killm --restore` — which also sweeps up rules left behind by a crash, since
205
+ they're all tagged).
206
+
207
+ Caveats, honestly stated:
138
208
 
139
- If something goes wrong and a block is left behind, `sudo npx killm --restore`
140
- (or the Administrator equivalent) cleans it up.
209
+ - IPs are captured **at block time**; if a provider rotates addresses
210
+ mid-block, new IPs aren't covered.
211
+ - Big providers sit behind shared CDNs — blocking their current IPs _may_
212
+ affect unrelated sites served from the same edge.
213
+ - Inside WSL, iptables rules (like the hosts file) only affect WSL traffic,
214
+ not Windows apps.
141
215
 
142
216
  ## Limitations & honesty
143
217
 
package/dist/src/cli.js CHANGED
@@ -23,6 +23,9 @@ SCOPE (pick one or more; default is --all)
23
23
  --all Block both of the above. This is the default if no scope is given.
24
24
 
25
25
  OPTIONS
26
+ --firewall Also block the targets' current IPs at the OS firewall
27
+ (iptables/ip6tables, Windows Firewall, or pf). Catches
28
+ browsers that bypass the hosts file via Secure DNS (DoH).
26
29
  --restore Remove any active killm block right now and exit.
27
30
  --status Show whether a block is currently active and exit.
28
31
  --list Print the hostnames that would be blocked and exit.
@@ -50,6 +53,7 @@ export function parseArgs(argv) {
50
53
  scope: { agents: false, web: false, all: false },
51
54
  dryRun: false,
52
55
  yes: false,
56
+ firewall: false,
53
57
  };
54
58
  let explicit;
55
59
  let durationInput;
@@ -74,6 +78,9 @@ export function parseArgs(argv) {
74
78
  case '--dry-run':
75
79
  result.dryRun = true;
76
80
  break;
81
+ case '--firewall':
82
+ result.firewall = true;
83
+ break;
77
84
  case '-y':
78
85
  case '--yes':
79
86
  result.yes = true;
@@ -0,0 +1,175 @@
1
+ import { execFile } from 'node:child_process';
2
+ import dns from 'node:dns/promises';
3
+ /**
4
+ * Firewall-level blocking, layered on top of the hosts-file block.
5
+ *
6
+ * The hosts file only intercepts name resolution, so a browser with Secure
7
+ * DNS (DoH) enabled sails right past it. Firewall mode resolves each target
8
+ * hostname to its current IPs and blocks those at the OS firewall:
9
+ *
10
+ * - Linux: iptables / ip6tables OUTPUT rules tagged with a "killm" comment
11
+ * - Windows: a single "killm" outbound netsh advfirewall rule
12
+ * - macOS: a pf anchor named "killm"
13
+ *
14
+ * All rules are identifiable without local state, so `killm --restore` can
15
+ * always clean up, even after a crash.
16
+ */
17
+ export const RULE_TAG = 'killm';
18
+ /** Maximum IPs per netsh rule; Windows accepts a comma list but not unbounded. */
19
+ const WINDOWS_CHUNK = 100;
20
+ export const defaultRunner = (cmd, args, stdin) => new Promise((resolve) => {
21
+ const child = execFile(cmd, args, (error, stdout) => resolve({ ok: !error, stdout: stdout ?? '' }));
22
+ if (stdin != null && child.stdin) {
23
+ child.stdin.write(stdin);
24
+ child.stdin.end();
25
+ }
26
+ });
27
+ /**
28
+ * Resolve every hostname to its current A/AAAA records. Failures are
29
+ * collected, not thrown — an unresolvable host simply can't be blocked by IP.
30
+ */
31
+ export async function resolveTargetIPs(hosts, resolver = dns) {
32
+ const v4 = new Set();
33
+ const v6 = new Set();
34
+ const unresolved = [];
35
+ await Promise.all(hosts.map(async (host) => {
36
+ const [a, aaaa] = await Promise.all([
37
+ resolver.resolve4(host).catch(() => []),
38
+ resolver.resolve6(host).catch(() => []),
39
+ ]);
40
+ if (a.length === 0 && aaaa.length === 0) {
41
+ unresolved.push(host);
42
+ return;
43
+ }
44
+ for (const ip of a)
45
+ v4.add(ip);
46
+ for (const ip of aaaa)
47
+ v6.add(ip);
48
+ }));
49
+ return { v4: Array.from(v4).sort(), v6: Array.from(v6).sort(), unresolved: unresolved.sort() };
50
+ }
51
+ // ---- pure command builders (unit-tested per platform) --------------------
52
+ /** iptables/ip6tables arguments to add one REJECT rule tagged with killm. */
53
+ export function linuxAddArgs(ip) {
54
+ return ['-I', 'OUTPUT', '-d', ip, '-j', 'REJECT', '-m', 'comment', '--comment', RULE_TAG];
55
+ }
56
+ /**
57
+ * Convert `iptables -S OUTPUT` output into the `-D` argument lists needed to
58
+ * delete every killm-tagged rule. Our rules never contain quoted spaces, so
59
+ * whitespace splitting is safe.
60
+ */
61
+ export function linuxDeleteArgsFromListing(listing) {
62
+ const out = [];
63
+ for (const line of listing.split('\n')) {
64
+ const trimmed = line.trim();
65
+ if (!trimmed.startsWith('-A OUTPUT'))
66
+ continue;
67
+ if (!trimmed.includes(`--comment ${RULE_TAG}`))
68
+ continue;
69
+ out.push(['-D', ...trimmed.slice('-A '.length).split(/\s+/)]);
70
+ }
71
+ return out;
72
+ }
73
+ /** netsh argument lists to add the killm outbound block rule(s). */
74
+ export function windowsAddArgs(ips) {
75
+ const out = [];
76
+ for (let i = 0; i < ips.length; i += WINDOWS_CHUNK) {
77
+ const chunk = ips.slice(i, i + WINDOWS_CHUNK);
78
+ out.push([
79
+ 'advfirewall',
80
+ 'firewall',
81
+ 'add',
82
+ 'rule',
83
+ `name=${RULE_TAG}`,
84
+ 'dir=out',
85
+ 'action=block',
86
+ `remoteip=${chunk.join(',')}`,
87
+ ]);
88
+ }
89
+ return out;
90
+ }
91
+ /** netsh argument list that deletes every rule named killm. */
92
+ export function windowsDeleteArgs() {
93
+ return ['advfirewall', 'firewall', 'delete', 'rule', `name=${RULE_TAG}`];
94
+ }
95
+ /** pf rule text for the killm anchor on macOS. */
96
+ export function darwinAnchorRules(v4, v6) {
97
+ const all = [...v4, ...v6];
98
+ if (all.length === 0)
99
+ return '';
100
+ return `block drop out quick to { ${all.join(', ')} }\n`;
101
+ }
102
+ /**
103
+ * Resolve the hostnames and install firewall rules for their current IPs.
104
+ * Throws on unsupported platforms; individual rule failures are tolerated.
105
+ */
106
+ export async function applyFirewall(hosts, opts = {}) {
107
+ const runner = opts.runner ?? defaultRunner;
108
+ const platform = opts.platform ?? process.platform;
109
+ const { v4, v6, unresolved } = await resolveTargetIPs(hosts, opts.resolver);
110
+ const blocked = v4.length + v6.length;
111
+ // Individual rule failures are tolerated, but if not a single command
112
+ // succeeded the firewall tool itself is missing/unusable — surface that
113
+ // instead of pretending the IPs are blocked.
114
+ let applied = 0;
115
+ const failIfNothingApplied = () => {
116
+ if (blocked > 0 && applied === 0) {
117
+ throw new Error('no firewall rules could be applied (is the firewall tool available?)');
118
+ }
119
+ return { blocked, unresolved };
120
+ };
121
+ if (platform === 'linux') {
122
+ for (const ip of v4)
123
+ if ((await runner('iptables', linuxAddArgs(ip))).ok)
124
+ applied++;
125
+ for (const ip of v6)
126
+ if ((await runner('ip6tables', linuxAddArgs(ip))).ok)
127
+ applied++;
128
+ return failIfNothingApplied();
129
+ }
130
+ if (platform === 'win32') {
131
+ for (const args of windowsAddArgs([...v4, ...v6])) {
132
+ if ((await runner('netsh', args)).ok)
133
+ applied++;
134
+ }
135
+ return failIfNothingApplied();
136
+ }
137
+ if (platform === 'darwin') {
138
+ const rules = darwinAnchorRules(v4, v6);
139
+ if (rules) {
140
+ await runner('pfctl', ['-e']); // best effort: enable pf if it isn't
141
+ if ((await runner('pfctl', ['-a', RULE_TAG, '-f', '-'], rules)).ok)
142
+ applied++;
143
+ }
144
+ return failIfNothingApplied();
145
+ }
146
+ throw new Error(`--firewall is not supported on platform "${platform}"`);
147
+ }
148
+ /**
149
+ * Remove every killm firewall rule. Needs no stored state, so it also cleans
150
+ * up rules left behind by a crashed run. Failures are tolerated — rules that
151
+ * don't exist can't be removed.
152
+ */
153
+ export async function removeFirewall(opts = {}) {
154
+ const runner = opts.runner ?? defaultRunner;
155
+ const platform = opts.platform ?? process.platform;
156
+ if (platform === 'linux') {
157
+ for (const tool of ['iptables', 'ip6tables']) {
158
+ const listing = await runner(tool, ['-S', 'OUTPUT']);
159
+ if (!listing.ok)
160
+ continue;
161
+ for (const args of linuxDeleteArgsFromListing(listing.stdout)) {
162
+ await runner(tool, args);
163
+ }
164
+ }
165
+ return;
166
+ }
167
+ if (platform === 'win32') {
168
+ await runner('netsh', windowsDeleteArgs());
169
+ return;
170
+ }
171
+ if (platform === 'darwin') {
172
+ await runner('pfctl', ['-a', RULE_TAG, '-F', 'rules']);
173
+ return;
174
+ }
175
+ }
package/dist/src/hosts.js CHANGED
@@ -4,6 +4,8 @@ import path from 'node:path';
4
4
  import { execFile } from 'node:child_process';
5
5
  export const BEGIN = '# >>> killm block (do not edit between markers) >>>';
6
6
  export const END = '# <<< killm block <<<';
7
+ /** Machine-parseable expiry marker inside the block. */
8
+ export const EXPIRES_PREFIX = '# killm-expires: ';
7
9
  const SINK4 = '0.0.0.0';
8
10
  const SINK6 = '::1';
9
11
  function errnoCode(err) {
@@ -59,6 +61,7 @@ export function buildBlock(hosts, opts = {}) {
59
61
  lines.push(`# added by killm at ${new Date().toISOString()}`);
60
62
  if (opts.until) {
61
63
  lines.push(`# auto-removed at ${opts.until.toISOString()} (or when killm exits)`);
64
+ lines.push(`${EXPIRES_PREFIX}${opts.until.toISOString()}`);
62
65
  }
63
66
  for (const h of hosts) {
64
67
  lines.push(`${SINK4}\t${h}`);
@@ -125,6 +128,36 @@ export function removeBlock() {
125
128
  export function isBlocked() {
126
129
  return readHosts().includes(BEGIN);
127
130
  }
131
+ /**
132
+ * When the current block is scheduled to expire, or null when no block is
133
+ * present or it carries no (valid) expiry marker — e.g. blocks written by
134
+ * killm <= 1.0.2.
135
+ */
136
+ export function blockExpiry(text = readHosts()) {
137
+ const begin = text.indexOf(BEGIN);
138
+ if (begin === -1)
139
+ return null;
140
+ const idx = text.indexOf(EXPIRES_PREFIX, begin);
141
+ if (idx === -1)
142
+ return null;
143
+ const start = idx + EXPIRES_PREFIX.length;
144
+ const lineEnd = text.indexOf('\n', start);
145
+ const stamp = text.slice(start, lineEnd === -1 ? undefined : lineEnd).trim();
146
+ const date = new Date(stamp);
147
+ return Number.isNaN(date.getTime()) ? null : date;
148
+ }
149
+ /**
150
+ * Remove the block if its expiry has passed — heals hosts files stranded by
151
+ * a killed process (terminal closed, kill -9, crash, machine shutdown).
152
+ *
153
+ * @returns true when an expired block was found and removed
154
+ */
155
+ export function clearExpiredBlock(now = new Date()) {
156
+ const expiry = blockExpiry();
157
+ if (expiry === null || expiry.getTime() > now.getTime())
158
+ return false;
159
+ return removeBlock();
160
+ }
128
161
  /**
129
162
  * Best-effort DNS cache flush so the new hosts entries take effect immediately.
130
163
  * Silently ignores failures — the block still works, it may just take a moment.
@@ -159,3 +192,18 @@ export function hasPrivileges() {
159
192
  return true;
160
193
  return typeof process.getuid === 'function' && process.getuid() === 0;
161
194
  }
195
+ /**
196
+ * Whether we are running inside Windows Subsystem for Linux. A block applied
197
+ * here edits WSL's /etc/hosts only — Windows browsers read the Windows hosts
198
+ * file and are NOT affected.
199
+ */
200
+ export function isWsl(procVersionPath = '/proc/version') {
201
+ if (process.platform !== 'linux')
202
+ return false;
203
+ try {
204
+ return /microsoft/i.test(fs.readFileSync(procVersionPath, 'utf8'));
205
+ }
206
+ catch {
207
+ return false;
208
+ }
209
+ }
package/dist/src/index.js CHANGED
@@ -3,6 +3,7 @@ import { parseArgs, HELP, VERSION } from './cli.js';
3
3
  import { resolveTargets } from './targets.js';
4
4
  import { formatDuration } from './duration.js';
5
5
  import * as hosts from './hosts.js';
6
+ import * as firewall from './firewall.js';
6
7
  // Minimal ANSI styling that degrades to plain text when not a TTY.
7
8
  const tty = process.stdout.isTTY === true;
8
9
  const c = {
@@ -93,6 +94,15 @@ async function runBlock(parsed) {
93
94
  return 0;
94
95
  }
95
96
  }
97
+ // Heal any expired block stranded by a killed process before applying ours.
98
+ try {
99
+ if (hosts.clearExpiredBlock()) {
100
+ out(c.dim(' (cleaned up an expired block left behind by a previous run)'));
101
+ }
102
+ }
103
+ catch {
104
+ /* no privileges; the apply below will surface the real error */
105
+ }
96
106
  const endTime = Date.now() + durationMs;
97
107
  const until = new Date(endTime);
98
108
  try {
@@ -110,6 +120,37 @@ async function runBlock(parsed) {
110
120
  }
111
121
  await hosts.flushDns();
112
122
  out(c.green(` ✓ block active until ${until.toLocaleTimeString()}`));
123
+ let firewallActive = false;
124
+ if (parsed.firewall) {
125
+ try {
126
+ const result = await firewall.applyFirewall(targets);
127
+ firewallActive = true;
128
+ out(c.green(` ✓ firewall: blocked ${result.blocked} IPs at the OS firewall`));
129
+ if (result.unresolved.length > 0) {
130
+ out(c.dim(` (${result.unresolved.length} hostnames did not resolve and were skipped)`));
131
+ }
132
+ }
133
+ catch (e) {
134
+ err(c.yellow(` ⚠ firewall rules could not be applied: ${e.message}`));
135
+ err(c.yellow(' Continuing with the hosts-file block only.'));
136
+ }
137
+ }
138
+ if (hosts.isWsl()) {
139
+ out();
140
+ out(c.yellow(' ⚠ WSL detected: this only blocks processes running inside WSL.'));
141
+ out(c.yellow(' Windows apps (your browser!) read the Windows hosts file and are'));
142
+ out(c.yellow(' NOT affected. To block them, run killm from an Administrator'));
143
+ out(c.yellow(' PowerShell/terminal on Windows: npx killm for 1h --web'));
144
+ }
145
+ out();
146
+ out(c.dim(' note: browsers cache DNS and "Secure DNS" (DoH) bypasses the hosts') +
147
+ '\n' +
148
+ c.dim(' file entirely — restart the browser / disable Secure DNS if needed.'));
149
+ out();
150
+ out(c.yellow(' ⚠ keep this process running — killm lifts the block when the timer'));
151
+ out(c.yellow(' ends or you press Ctrl+C / close the terminal. If it dies harder'));
152
+ out(c.yellow(' than that (kill -9, crash, shutdown), the block stays in place'));
153
+ out(c.yellow(' until you run any killm command again, e.g. killm --restore'));
113
154
  out();
114
155
  let timer = null;
115
156
  let ticker = null;
@@ -123,6 +164,11 @@ async function runBlock(parsed) {
123
164
  clearTimeout(timer);
124
165
  if (ticker)
125
166
  clearInterval(ticker);
167
+ if (firewallActive) {
168
+ // Fire and forget: rules are tagged, so a missed removal here is still
169
+ // recoverable via "killm --restore".
170
+ void firewall.removeFirewall().catch(() => { });
171
+ }
126
172
  try {
127
173
  const removed = hosts.removeBlock();
128
174
  void hosts.flushDns(); // fire and forget on the way out
@@ -167,6 +213,9 @@ async function runBlock(parsed) {
167
213
  };
168
214
  process.once('SIGINT', () => onSignal('interrupted'));
169
215
  process.once('SIGTERM', () => onSignal('terminated'));
216
+ // Closing the terminal window sends SIGHUP; without this handler Node
217
+ // dies without cleanup and the block is stranded in the hosts file.
218
+ process.once('SIGHUP', () => onSignal('terminal closed'));
170
219
  });
171
220
  return 0;
172
221
  }
@@ -192,9 +241,31 @@ export async function main(argv) {
192
241
  out(VERSION);
193
242
  return 0;
194
243
  case 'status': {
244
+ const expiry = hosts.blockExpiry();
245
+ const expired = expiry !== null && expiry.getTime() <= Date.now();
246
+ if (expired) {
247
+ // A previous run was killed before it could clean up. Heal it now.
248
+ try {
249
+ hosts.clearExpiredBlock();
250
+ await hosts.flushDns();
251
+ out(c.green('killm: no block active.'));
252
+ out(c.dim('(cleaned up an expired block left behind by a previous run)'));
253
+ return 0;
254
+ }
255
+ catch {
256
+ err(c.yellow('killm: an EXPIRED block is still stuck in the hosts file.'));
257
+ err(' Clean it up with elevated privileges: sudo npx killm --restore');
258
+ return 1;
259
+ }
260
+ }
195
261
  if (hosts.isBlocked()) {
196
262
  out(c.yellow('killm: a block is currently ACTIVE.'));
197
- out(c.dim('Run "killm --restore" to lift it.'));
263
+ if (expiry) {
264
+ out(c.dim(`Scheduled to lift at ${expiry.toLocaleString()}.`));
265
+ out(c.dim('If the killm process that created it is no longer running, it will not\n' +
266
+ 'lift itself on time — but any later killm command cleans it up once expired.'));
267
+ }
268
+ out(c.dim('Run "killm --restore" to lift it now.'));
198
269
  }
199
270
  else {
200
271
  out(c.green('killm: no block active.'));
@@ -217,6 +288,13 @@ export async function main(argv) {
217
288
  try {
218
289
  const removed = hosts.removeBlock();
219
290
  await hosts.flushDns();
291
+ // Always sweep firewall rules too: they are tagged "killm", so this
292
+ // also cleans up after a crashed --firewall run. Harmless when none
293
+ // exist or the platform is unsupported. Skipped under the test
294
+ // override so tests never touch the real firewall.
295
+ if (!process.env.KILLM_HOSTS_PATH) {
296
+ await firewall.removeFirewall().catch(() => { });
297
+ }
220
298
  out(removed
221
299
  ? c.green('killm: block lifted. Access restored.')
222
300
  : c.dim('killm: no block was active.'));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "killm",
3
- "version": "1.0.0",
3
+ "version": "1.0.3",
4
4
  "description": "Temporarily block your machine from reaching LLM services to curb AI dependency. npx killm for 1h --agents",
5
5
  "bin": {
6
6
  "killm": "dist/src/bin.js"
@@ -15,7 +15,7 @@
15
15
  "build": "tsc",
16
16
  "typecheck": "tsc --noEmit",
17
17
  "pretest": "npm run build",
18
- "test": "node --test dist/test/duration.test.js dist/test/targets.test.js dist/test/cli.test.js dist/test/hosts.test.js dist/test/e2e.test.js",
18
+ "test": "node --test dist/test/duration.test.js dist/test/targets.test.js dist/test/cli.test.js dist/test/hosts.test.js dist/test/firewall.test.js dist/test/e2e.test.js",
19
19
  "lint": "eslint .",
20
20
  "lint:fix": "eslint . --fix",
21
21
  "format": "prettier --write .",