cligentic
← back to catalog
cligentic block

killswitch

Binary safety gate. If ~/.app/KILLSWITCH exists, all write operations refuse. File exists = stopped. File gone = resumed. One existsSync call. Battle-tested in hapi-cli for real brokerage orders.

Install
killswitch demo
NPM dependencies

zero deps — pure TS

Block dependencies

standalone

Size

112 LOC

The problem

An agent running your CLI starts doing something wrong. Maybe the market tanked and your trading bot is still buying. Maybe a deploy script is pushing to production when it shouldn't. You need to stop everything. Now.

The killswitch block is a binary safety gate. If a file exists at ~/.yourapp/KILLSWITCH, every write operation refuses to execute. Remove the file, operations resume. One touch command stops the world.

Quickstart

import { assertKillswitchOff, turnKillswitchOn } from "@/cli/safety/killswitch";
 
const APP_HOME = join(homedir(), ".myapp");
 
program.command("order").action(async (opts) => {
  assertKillswitchOff(APP_HOME);
  await placeOrder(opts);
});
 
program.command("stop").action(() => {
  turnKillswitchOn(APP_HOME, "manual emergency stop");
  console.log("All writes blocked.");
});

How it works

touch ~/.myapp/KILLSWITCH             # stop everything
cat ~/.myapp/KILLSWITCH               # see reason + timestamp
rm ~/.myapp/KILLSWITCH                # resume

One file. One existsSync call. No database. No network. No race conditions.

API

isKillswitchOn(appHome): boolean          // fast, never throws
getKillswitchState(appHome): KillswitchState  // includes reason + timestamp
turnKillswitchOn(appHome, reason?): void  // idempotent
turnKillswitchOff(appHome): void          // idempotent
assertKillswitchOff(appHome): void        // throws if active

Why file-based

  • Atomic: file exists or it doesn't. No partial states.
  • Cross-process: any terminal, any user, any script can create or remove it.
  • Debuggable: cat the file to see why and when.
  • Offline: no network, no database.
  • Fast: existsSync is < 1ms.

The safety stack

killswitch is the simplest piece of cligentic's safety layer. For CLIs that need more granularity, v0.1 will add trust-ladder, approvals, intent-token, and safety-middleware.

The killswitch is always checked first. If it's on, nothing else runs.

Source

The full file, 112 lines.

This is the exact TypeScript that lands in your project when you run the install command. Read it, copy it, edit it, own it. cligentic never touches it again.

src/cli/safety/killswitch.ts
// cligentic block: killswitch
//
// Binary safety gate. If a file exists at ~/.{app}/KILLSWITCH, all write
// operations refuse to execute. That's it. No config, no database, no
// network call. File exists = stopped. File gone = resumed.
//
// Design rules:
//   1. File existence is the sole source of truth. Atomic, no race conditions.
//   2. The file contains a JSON payload with reason + timestamp for debugging.
//   3. Checking the killswitch is a single fs.existsSync call (< 1ms).
//   4. Turning it on/off is idempotent.
//   5. Never throws from isKillswitchOn(). Always returns boolean.
//
// Usage:
//   import { assertKillswitchOff, turnKillswitchOn, turnKillswitchOff, isKillswitchOn } from "./safety/killswitch";
//
//   // Guard a write operation
//   assertKillswitchOff(appHome);  // throws if KILLSWITCH file exists
//   await placeOrder(order);
//
//   // Emergency stop from another terminal
//   turnKillswitchOn(appHome, "suspicious activity detected");
//
//   // Resume after investigation
//   turnKillswitchOff(appHome);

import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
import { join } from "node:path";

export type KillswitchState = {
  active: boolean;
  reason?: string;
  activatedAt?: string;
};

const KILLSWITCH_FILE = "KILLSWITCH";

function getPath(appHome: string): string {
  return join(appHome, KILLSWITCH_FILE);
}

/**
 * Check if the killswitch is active. Fast (single existsSync call).
 * Never throws.
 */
export function isKillswitchOn(appHome: string): boolean {
  try {
    return existsSync(getPath(appHome));
  } catch {
    return false;
  }
}

/**
 * Read the killswitch state including reason and timestamp.
 * Returns { active: false } if the file doesn't exist or can't be read.
 */
export function getKillswitchState(appHome: string): KillswitchState {
  const path = getPath(appHome);
  if (!existsSync(path)) return { active: false };
  try {
    const data = JSON.parse(readFileSync(path, "utf8"));
    return { active: true, reason: data.reason, activatedAt: data.at };
  } catch {
    return { active: true, reason: "unknown (file exists but unreadable)" };
  }
}

/**
 * Activate the killswitch. Idempotent. Creates the app home directory
 * if it doesn't exist. Writes reason + timestamp to the file.
 */
export function turnKillswitchOn(appHome: string, reason = "manual"): void {
  mkdirSync(appHome, { recursive: true });
  writeFileSync(
    getPath(appHome),
    JSON.stringify({ reason, at: new Date().toISOString() }, null, 2),
  );
}

/**
 * Deactivate the killswitch. Idempotent. No-op if already off.
 */
export function turnKillswitchOff(appHome: string): void {
  const path = getPath(appHome);
  if (existsSync(path)) {
    unlinkSync(path);
  }
}

/**
 * Assert that the killswitch is off. If it's on, throws an error with
 * the reason and timestamp. Use this as a guard at the top of any
 * write operation.
 *
 * ```ts
 * assertKillswitchOff(appHome);
 * await dangerousOperation();
 * ```
 */
export function assertKillswitchOff(appHome: string): void {
  const state = getKillswitchState(appHome);
  if (state.active) {
    const since = state.activatedAt ? ` since ${state.activatedAt}` : "";
    const why = state.reason ? `: ${state.reason}` : "";
    throw new Error(
      `Killswitch is ON${since}${why}. All write operations are blocked. ` +
        `Remove ${getPath(appHome)} or run your CLI's killswitch off command to resume.`,
    );
  }
}