人間失格

CI docs License: MIT

Burn-after-reading secrets for AI agents — keys live in locked, non-swappable RAM, are served to agents over MCP, and are destroyed after N reads, or the instant your session dies. Walk away: agents die, secrets burn.

Install

curl --proto '=https' --tlsv1.2 -LsSf https://github.com/New1Direction/ningen-shikkaku/releases/latest/download/dazai-installer.sh | sh
# or: brew install New1Direction/tap/dazai New1Direction/tap/motokano

See it burn

burn-after-reading demo

motokano --calls 1 --tool 'name=get_key,kind=static,value=s3cr3t' --arm

One read → the secret. Second read → the server has already wiped the value out of locked memory and SIGKILLed itself.

Wire it into an agent

claude mcp add burn-once -- motokano --calls 1 --arm \
  --tool 'name=get_key,kind=static,value=YOUR-SECRET'

Start here

  • Quickstart — install, rehearse in dry-run, then arm
  • Threat model — exactly what it protects against, and what it doesn't
  • GitHub — source, issues, CI

ningen-shikkaku/
  dazai        the daemon. the man.
  goodnight    secure memory layer
  kikka        the watchdog
  kekkai       the seccomp wall
  sienna       child process wrapper
  rei          MCP adapter
  motokano     one-shot server

Mine has been a life of much shame. No longer human. No longer here.

— Osamu Dazai, 1948

what it is

ningen-shikkaku is a dead-man's-switch for secret material. You give it a secret — an API key, a token, a credential — and a liveness signal tied to your session. While the signal is alive the secret lives in locked RAM. The moment the signal stops, the secret is destroyed and the processes holding it are killed.

That's the whole idea. Everything else is making the destroy step trustworthy: not paged to swap, not captured in a core dump, not left as a compiler-elided "wipe" that never ran, and not escapable by the confined process.

Why it exists

A long-running process that touches secrets is a liability the entire time it runs. The secret sits in heap, maybe gets copied to swap, maybe lands in a core dump on the next crash, and stays resident long after the last use. The usual answer — "remember to zero it" — is a wish, not a guarantee.

ningen-shikkaku shrinks the window (the secret is destroyed at session end, deterministically) and the surface (locked pages, no dumps, a seccomp syscall allowlist) in which plaintext is reachable.

Two layers

It ships in two layers

  • a hardened Rust daemon + tooling — the real implementation, with guarantees the CPython runtime cannot offer (non-elidable wipe, mlock, madvise(MADV_DONTDUMP), prctl, a seccomp allowlist);
  • a Python reference implementation — the original proof-of-concept that established the mechanism. It is a reference, not a hard guarantee (see honest limitations).

Scope, stated plainly

It runs only on your own machine

ningen-shikkaku operates on your own secrets, your own processes, and your own configured tools. It never touches another process, file, or host. It is defensive, operator-side tooling — a way to make your secrets ephemeral and session-bound — not a means of acting on anyone else's system.

It is not, and cannot be, a guarantee against an attacker who already owns your running machine. It is a sharp tool for making secrets ephemeral, with every limitation stated up front. The next pages show exactly how it works and exactly where it stops.

how it works

Three moving parts: secret memory that is hard to leak, a liveness channel that detects session loss, and a panic policy that destroys everything when liveness is lost.

1. Secrets live in locked memory

Secret material is held in a goodnight::SecretBuffer — an anonymous mmap mapping that is:

  • mlocked — pinned into physical RAM, never written to swap;
  • madvise(MADV_DONTDUMP) — excluded from core dumps (Linux);
  • move-only — no Clone/Copy, so the bytes are never silently duplicated onto a GC heap;
  • explicitly wipeable — overwritten with explicit_bzero (Linux) / memset_s (macOS) / a volatile loop, a wipe the optimizer is contractually forbidden to elide.

The process additionally raises RLIMIT_MEMLOCK and sets prctl(PR_SET_DUMPABLE, 0) (Linux) to disable core dumps and ptrace attachment.

2. A heartbeat tracks your session

The daemon binds a 0600 UNIX socket and listens. A client (dazai client, run from your shell or SSH session) connects and holds the connection open, optionally sending PING. That connection is the liveness signal:

  • the client process dies, the shell closes, the SSH session drops → the socket closes → liveness lost;
  • with --ping-timeout N, a missing PING for N seconds → liveness lost.

A single heartbeat is honored; a second concurrent one is refused with BUSY. See the socket protocol.

3. Loss of liveness triggers the panic

Liveness loss — or an explicit panic signal — runs the kill sequence: kill every registered process, wipe the secret buffers, then SIGKILL self.

Signals

  • SIGUSR1 → graceful panic (honors the grace window when armed)
  • SIGUSR2hard panic (bypasses the grace window)
  • SIGTERM / SIGINT → clean shutdown: wipe, but no kill

4. Confinement seals the daemon

On Linux, after binding the socket and allocating buffers — but before entering the event loop — the daemon installs a seccomp allowlist. Any syscall outside the allowlist (execve, open, connect, ptrace, …) terminates the process via KillProcess. The confinement is applied last so the locked, non-dumpable, syscall-restricted state is the state the daemon spends its whole life in.

Startup order matters

The order is a security property, not a style choice

  1. raise RLIMIT_MEMLOCK
  2. prctl(PR_SET_DUMPABLE, 0) (Linux)
  3. allocate + mlock the secret buffers
  4. spawn the child process (if --exec)
  5. bind the UNIX socket (0600) + write the pidfile (0600)
  6. apply seccomp (Linux + seccomp feature)
  7. enter the accept/event loop

Buffers are locked before anything can fault them to disk; seccomp is applied after every privileged setup syscall is done, so the allowlist can be as small as the steady-state loop needs.

the kill sequence

When liveness is lost or a panic signal arrives, the PanicController runs a fixed, single-fire sequence. The order is deliberate: kill first, wipe second, die last.

Armed, graceful panic

On any armed trigger

  1. kill registered processes. Every PID registered with the daemon is SIGKILLed first, while the daemon is still alive to do it. A child spawned via --exec is killed the same way.
  2. wipe the secret buffers. Each SecretBuffer is overwritten with a non-elidable wipe (explicit_bzero / memset_s / volatile loop).
  3. remove the pidfile, then SIGKILL self. The daemon does not return from this.

Killing the holders before wiping means no registered process can read a half-wiped buffer; wiping before self-kill means the wipe is the last meaningful thing the process does.

The grace window

When the daemon is armed (--arm) a graceful panic (heartbeat drop, --ping-timeout, SIGUSR1) starts a grace window of --grace seconds. The sequence fires when the window expires. A genuine heartbeat reconnect during the window cancels it — a transient blip does not destroy your secrets.

Hard panic bypasses grace

SIGUSR2 is a hard panic: it skips the grace window entirely and runs the sequence immediately. Use it when you want destruction now, not in --grace seconds.

Single-fire

The controller is gated by an atomic flag and a compare_exchange: no matter how many triggers race (a dropped socket and a signal and a timeout in the same instant), the sequence runs exactly once.

Dry-run is the default

Nothing is killed until you --arm it

Without --arm, the daemon runs in dry-run: it performs the wipe and logs WOULD SIGKILL … for every process it would have killed, but never actually sends a kill. This lets you rehearse the full trigger path — heartbeat loss, grace window, reconnect-cancel — with zero risk before arming for real.

What can defeat it

The sequence is best-effort against paths where the wipe code never gets to run: an external kill -9, a kernel OOM kill, or power loss. On those paths nothing is zeroed, and the project never claims otherwise — see honest limitations.

threat model

ningen-shikkaku shrinks the window and the surface in which plaintext secrets are reachable, and makes session-end deterministically destroy them. It is precise about what that does and does not buy you.

What it protects against

RiskHow
Secrets paged to swapmlock pins the buffers into RAM
Secrets captured in core dumpsmadvise(MADV_DONTDUMP) + prctl(PR_SET_DUMPABLE, 0) (Linux)
Session loss leaving secrets residentheartbeat drop / logout → wipe + SIGKILL
Secrets lingering in process memory after useexplicit_bzero / memset_s — a wipe the compiler may not elide
A confined process escapingseccomp allowlist denies execve / open / connect / ptrace / … (Linux)
ptrace snooping the daemonprctl(PR_SET_DUMPABLE, 0) (Linux)

What it does NOT protect against

Out of scope — by design, and stated up front

  • GPU VRAM. If an attached agent copies a secret into GPU memory, killing the host process does not wipe VRAM. ningen-shikkaku controls host RAM and the processes it supervises — not an accelerator's memory.
  • exec-tool stdout. motokano's kind=exec tools return a command's stdout, which is OS-buffered and not held in a locked buffer. Use kind=static when you need the wipe guarantee.
  • Managed-runtime residue (Python reference). CPython copies bytes freely, so a secret may transiently live in unlocked heap. The Rust layer eliminates this for its own buffers; the Python tier is a reference, not a guarantee.
  • A privileged or same-UID attacker on the live box (root, /proc/<pid>/mem, a debugger) reading memory before the wipe. mlock stops swap, not reads.
  • Cold-boot / DMA / physical attacks on RAM.
  • A hard crash (kernel OOM, external kill -9, power loss): the wipe path can't run, so nothing is zeroed.

The honest summary

Quote

ningen-shikkaku is not, and cannot be, a guarantee against an attacker who already owns your running machine. It is a sharp tool for making secrets ephemeral and session-bound, with every limitation stated up front.

How the guarantees are checked

  • 66 tests across the Rust workspace (secure memory, the panic policy, the wire protocol, the MCP layer, the one-shot lifecycle), plus the 29-test Python reference.
  • Linux seccomp is validated on both architectures under the real KillProcess filteraarch64 locally and x86_64 in CI — where the daemon installs the live filter and the full integration suite runs against it with no SIGSYS.
  • Every push runs the whole suite (default and --features seccomp), clippy -D warnings, and rustfmt --check as a permanent regression gate.

dazai — the daemon

dazai is the CLI binary and the watchdog that everything else orbits. It holds the secret buffers, owns the heartbeat socket, and runs the kill sequence.

Subcommands

CommandRole
dazai daemonthe watchdog: holds mlock'd secrets + a UNIX-socket heartbeat, wipes and self-destructs on session loss, seccomp-confined on Linux
dazai clientthe heartbeat client — ties the daemon's life to a shell / SSH session
dazai mcpthe MCP adapter — exposes the daemon as tools any agent can call

Startup order

The daemon performs privileged setup in a fixed order, then confines itself before serving:

  1. raise RLIMIT_MEMLOCK
  2. prctl(PR_SET_DUMPABLE, 0) (Linux)
  3. allocate + mlock the secret buffers (goodnight)
  4. spawn the LLM child if --exec (sienna)
  5. bind the 0600 UNIX socket + write the 0600 pidfile
  6. apply seccomp (kekkai, Linux + seccomp feature)
  7. enter the accept/event loop (kikka)

Why the pidfile

The daemon writes <socket>.pid (mode 0600) before applying seccomp — creating a file needs openat, which the filter denies. The MCP adapter reads it to find the daemon for dazai_panic / dazai_hard_panic.

Defaults

  • socket: ${XDG_RUNTIME_DIR:-/tmp}/dazai-$UID.sock, mode 0600
  • pidfile: <socket>.pid, mode 0600
  • mode: dry-run unless --arm
  • grace: 5 seconds (armed graceful panic)

See configuration for the full flag list.

Memory safety

dazai itself is #![deny(unsafe_code)]. Every unsafe operation it needs — locking memory, wiping, sending signals — lives behind the safe API of goodnight, the one crate permitted unsafe.

goodnight — secure memory

goodnight is the secure-memory layer and the only crate in the workspace permitted to use unsafe. Every other crate is #![deny(unsafe_code)] and reaches the platform through goodnight's safe API.

SecretBuffer

The core type. A SecretBuffer owns an anonymous mmap mapping and guarantees, for as long as it lives:

  • no swap — the pages are mlocked into RAM;
  • no core dumpmadvise(MADV_DONTDUMP) on Linux;
  • no silent copies — the type is move-only (no Clone, no Copy); data is written into the locked mapping through borrow-checked slices, never duplicated onto a GC or general heap;
  • a wipe on dropDrop overwrites the mapping before unmapping it.

API

new(len) · write(bytes) · as_slice() / as_mut_slice() · wipe() · len() / is_locked() · Drop

The wipe

The wipe is the whole point, so it is the part the optimizer is forbidden to remove:

PlatformMechanism
Linuxexplicit_bzero
macOSmemset_s
fallbackvolatile write loop

A plain memset can be optimized away as a "dead store" when the compiler sees the buffer is about to be freed. These three are contractually non-elidable.

Process-level helpers

goodnight also wraps the privileged syscalls the daemon needs, each behind a safe function:

  • try_raise_memlock_rlimit() — raise RLIMIT_MEMLOCK toward RLIM_INFINITY
  • set_process_undumpable()prctl(PR_SET_DUMPABLE, 0) (disables core dumps + ptrace)
  • current_uid(), pid_exists(), send_signal(), sigkill_pid(), raise_sigkill()

pid hygiene

The pid helpers reject pid == 0 and pid > i32::MAX before issuing any kill, so a malformed or wildcard PID can never be turned into a broadcast signal.

Why one unsafe crate

Confining every unsafe block to one small, audited crate means the memory-safety surface of the entire system is a few hundred reviewable lines — and the rest of the workspace is statically guaranteed safe by the compiler.

kikka — the watchdog

kikka is the liveness engine and the panic policy. It owns the socket listener, the signal thread, and the event loop — and it decides when to die and in what order. It is #![deny(unsafe_code)].

Watchdog

The Watchdog accepts connections on the daemon's UNIX socket and turns them into liveness events:

  • the first verb on a connection decides its kind — HELLO makes it the single heartbeat client; anything else makes it a control connection (protocol);
  • heartbeat liveness is tracked with a generation tag, so a stale reader thread from an old connection can never cancel a panic armed by a newer one;
  • with --ping-timeout, each connection carries a per-line ping deadline;
  • control connections are request/response, capped, and never touch the single-client heartbeat lock.

Registered PIDs are kept in a plain Vec<u32> (capped at 32) — they are not secret, so they are not stored in a SecretBuffer.

PanicController

The policy object. It is constructed with injected closures — wipe, kill, kill_registered, dry_done, clock, log — so the mechanism (how to wipe, how to kill) is decoupled from the policy (the order, the grace window, the single-fire guarantee). That separation is also what makes the whole kill sequence unit-testable without ever locking real memory or killing a real process.

The fixed sequence

kill_registeredwipekill self. Registered holders die before the buffers are wiped; the buffers are wiped before the daemon kills itself.

Single-fire

compare_exchange

The controller holds an Arc<AtomicBool> armed flag and fires through a compare_exchange. Concurrent triggers — a dropped socket and a signal and a timeout at once — collapse to exactly one run of the sequence.

Grace and cancel

When armed, a graceful trigger starts the --grace window. Only a real heartbeat reconnect after a loss cancels it; an ordinary message on an already-live connection does not. A hard panic (SIGUSR2) ignores the window.

kekkai — the seccomp wall

kekkai (結界, a warding barrier) is the syscall confinement layer. On Linux, with the seccomp feature, it installs a seccomp-bpf allowlist whose default action is KillProcess: any syscall not on the list terminates the daemon. Elsewhere — non-Linux, or without the feature — it compiles to a no-op stub, so the rest of the workspace is platform-agnostic. It is #![deny(unsafe_code)] (the filter is built through the safe libseccomp crate).

Default-deny

KillProcess, not errno

The filter's default action is KillProcess — a denied syscall does not return EPERM for the program to ignore, it kills the whole thread group immediately. The daemon either runs inside the allowlist or it does not run at all.

Two tiers of allowlist

TierBehavior
corethe syscalls the steady-state loop cannot live without — resolved with ?, so a name that fails to resolve is a hard error
runtimebest-effort extras (allocator, threading, signal plumbing) — names absent on an architecture are simply skipped, not fatal

This split keeps the allowlist honest on both aarch64 and x86_64: the must-haves are guaranteed, and arch-specific names (e.g. arch_prctl on x86_64) are added defensively without breaking the other arch.

Applied last

The daemon applies the filter after every privileged setup syscall (locking memory, binding the socket, writing the pidfile) and before the event loop. That ordering is what lets the allowlist stay small: it only has to cover serving, not setup.

Validated under the live filter, not emulated

seccomp is confirmed on both architectures running the real KillProcess filter — aarch64 locally and x86_64 in CI — where the daemon installs the filter and the full suite runs against it with no SIGSYS. Emulation (Rosetta / qemu-user) cannot install a guest seccomp filter and therefore cannot validate this; native runs are the only valid check, and CI is the permanent gate.

sienna — child process wrapper

sienna wraps a child process the daemon supervises — typically an LLM runtime launched with dazai daemon --exec /path/to/llm. The principle is parent-owns-the-kill-switch: the daemon spawns the child, holds its PID, and is the only thing that decides when it dies. It is #![deny(unsafe_code)].

ChildProcess

  • spawn(path, args) — launches the child via the standard library's Command (fork + exec), with no shell in between, so there is no argument string for an attacker to inject into;
  • none() — the empty case, when the daemon runs without --exec;
  • on any trigger, the daemon SIGKILLs the child first, as step one of the kill sequence, before wiping its own buffers.

No shell, no injection

The child is executed directly from a path and an argument vector — never sh -c "…". A configured command cannot be turned into shell metacharacter injection because there is no shell to interpret them.

Why a parent-owned child at all

An LLM or tool runtime that handles secrets should not outlive the session that authorized it. By making the daemon the parent, the child's lifetime is bound to the daemon's: when the heartbeat drops, the child is killed in the same deterministic sequence that wipes the secrets it was using.

MCP is the looser-coupled alternative

--exec supervises one child the daemon launches itself. When you want any number of independently-launched agents to opt into the same protection, use the MCP adapter instead — agents register their own PID and get the identical hard-kill guarantee. The two mechanisms coexist.

rei — MCP adapter

rei exposes the daemon as a set of MCP tools, so any MCP client — an agent, a tool runner, a swarm — can opt into session-bound protection. It adds zero new mechanism: it is a thin adapter (built on the rmcp SDK) that relays tool calls to the daemon socket and reads the pidfile to signal it. Run it with dazai mcp. It is #![deny(unsafe_code)].

Why MCP over --exec

--exec supervises one child the daemon launches. MCP inverts that: agents opt in by registering their own PID over a standard protocol. Protection becomes:

  • loosely coupled — the daemon and the agent launch independently;
  • stack-wide — any number of agents/tools register (capped at 32);
  • still hard-killing — a registered PID is SIGKILLed by the daemon on trigger exactly as an --exec child would be.

The contract

An agent does not need to know how ningen-shikkaku launches. ningen-shikkaku does not need to know anything about the agent. The only handshake is: register my PID; kill me if the session dies.

Tools

ToolEffectReturns
dazai_status()STATUS round-trip (a dead daemon is a valid answer){alive, armed, grace_seconds, registered_pids}
dazai_register(pid)register a PID for session-bound protection{ok, message}
dazai_unregister(pid)drop a registration{ok, message}
dazai_arm()arm the daemon at runtime{armed, message}
dazai_panic()SIGUSR1 to the daemon (graceful){triggered}
dazai_hard_panic()SIGUSR2 to the daemon (bypass grace){triggered}

Full schemas in the MCP tools reference; the wire side is in the socket protocol.

Transport

dazai mcp speaks MCP over stdio, the standard transport — point any MCP client at the command. It relays each call to the daemon's UNIX socket and, for panics, signals the PID it reads from the daemon's pidfile.

Stale-pidfile safety

A panic is gated on the daemon actually being alive (STATUS first). If the daemon has exited and its PID was recycled by the OS, rei will not blindly signal the recycled PID.

motokano — one-shot server

motokano is a standalone, self-immolating MCP server: it serves a configurable number of tool calls, then wipes its in-memory secret state and exits. It is the sharpest expression of the whole project — a secret server whose normal, expected end state is gone. It adds no new mechanism (static values live in goodnight::SecretBuffers; the optional daemon link reuses the control protocol) and is #![deny(unsafe_code)].

Tools

KindBehaviorWipe guarantee
staticserves a pre-loaded value held in a locked SecretBufferyes — wiped on exit
execruns an operator-configured command (no shell) and returns its stdoutno — stdout is OS-buffered, not locked

exec stdout is not locked

kind=exec is for dynamic values you don't control ahead of time. Its output is not in a locked buffer and is not covered by the wipe. When you need the wipe guarantee, use kind=static.

Three death conditions

Whichever fires first ends the process:

FlagCondition
--calls N (default 1)exit after N tool calls complete
--sessionexit when the client disconnects (stdin EOF)
--dazai-socket PATHregister with a dazai daemon and self-destruct if it dies

--calls and --session together mean exit on either.

The exit is the product

No secret served after the exit fires

The call counter is a single-fire compare_exchange, and a closed flag is set synchronously the instant the final call is accounted for. Any tool call arriving during the brief response-flush + grace window after exit fires is rejected — the secret cannot be served once the server has committed to dying. All paths funnel through one wipe routine guarded so it runs exactly once, ending in the buffer wipe and process exit.

Demo

motokano --calls 1 \
  --tool 'name=get_key,kind=static,value=s3cr3t' \
  --arm

Point an MCP client at it, call get_key once → you receive s3cr3t → the server wipes the value out of locked memory and SIGKILLs itself. Call again → the process is gone.

quickstart

Install

You need a recent Rust toolchain. On Linux, install libseccomp-dev + pkg-config to build the seccomp-confined daemon.

git clone https://github.com/New1Direction/ningen-shikkaku
cd ningen-shikkaku/rs
cargo build --release                       # -> target/release/{dazai, motokano}
cargo build --release --features seccomp    # Linux: with the seccomp allowlist

Rehearse safely (dry-run)

Dry-run is the default — it performs the wipe and logs what it would kill, but never sends a real kill. Use two terminals:

# terminal A — the daemon
dazai daemon --ping-timeout 15

# terminal B — the heartbeat client
dazai client --interval 5

Now close terminal B (or Ctrl-C the client). The daemon detects the dropped heartbeat, wipes its secret buffers, logs WOULD SIGKILL …, and exits cleanly. You have just watched the full trigger path with zero risk.

Try the timeout path too

Instead of closing the client, stop sending pings (kill just the client process) and watch --ping-timeout 15 fire after 15 seconds. Reconnect a client before the grace window expires (when armed) to see a cancel.

Arm it for real

--arm sends real SIGKILLs

With --arm, triggers actually kill registered processes and the daemon itself. Rehearse in dry-run first.

dazai daemon --arm --grace 5 --ping-timeout 15 --exec /path/to/llm

The one-shot, in one line

motokano --calls 1 --tool 'name=get_key,kind=static,value=s3cr3t' --arm

Point any MCP client at it, call get_key once → you get the value → it wipes and exits. See motokano.

Next

red team usage

ningen-shikkaku is operator-side tooling for an authorized engagement: it makes your own engagement secrets — the credentials, tokens, and keys issued to you for the work — ephemeral and bound to your session, on your own operator host.

Scope

Everything here runs on your machine, on your secrets, against processes you launched. ningen-shikkaku never reaches another host, and nothing on this page is about acting on a target. It is about not leaving your own credentials lying in memory after the session that authorized them ends.

The problem it solves for an operator

During an engagement your operator box accumulates secrets in long-running processes: a C2 operator console, a tool runner, a notebook holding a borrowed credential. If that box is lost, imaged, or crash-dumped, those secrets are recoverable from memory or swap. ningen-shikkaku ties their lifetime to your session and destroys them deterministically when it ends.

Pattern: session-bound credential

# hold the engagement key in locked, non-dumpable RAM, armed, with a short grace
dazai daemon --arm --grace 3 --ping-timeout 20

# from your operator shell, hold the heartbeat
dazai client --interval 5

Close the shell, lose the SSH session, or trip the timeout → the key is wiped and the daemon dies within the grace window. No swap copy, no core dump, nothing to seize.

Pattern: protect the whole tool stack

Register every engagement process so they all die together when the session ends — see MCP agent integration. A registered PID is SIGKILLed by the daemon on trigger, so a forgotten tool process can't outlive the engagement.

Pattern: one-shot credential handoff

When a tool needs a secret exactly once, serve it with motokano and let it self-destruct:

motokano --calls 1 --session \
  --tool 'name=token,kind=static,value=<engagement-token>' --arm

The value lives in a locked buffer, is served once, then wiped — and the server also dies if the daemon it's linked to dies.

Read the limits

This protects host RAM and the processes ningen-shikkaku supervises. It does not cover GPU VRAM, exec-tool stdout, or a privileged attacker already on the live box. Know exactly where it stops — see honest limitations.

MCP agent integration

Any MCP client can opt into session-bound protection by registering its own PID with the daemon. When your session dies, the daemon SIGKILLs every registered PID, then wipes and kills itself. This is the rei adapter in practice.

Wire it up

# terminal 1 — the daemon (writes <socket>.pid, 0600)
dazai daemon --arm --grace 5

# terminal 2 — the MCP server over stdio
dazai mcp

Point your MCP client at the dazai mcp command. The agent then calls the tools directly.

The registration handshake

agent → dazai_register(pid = <the agent's own PID>)
        ← { ok: true, message: "registered" }

agent → dazai_status()
        ← { alive: true, armed: true, grace_seconds: 5, registered_pids: 1 }

From here the agent does nothing special — it just works. If the operator's session dies, the daemon kills the registered PID as step one of the kill sequence.

Up to 32 registrants

Any number of agents and tools can register, capped at 32. Each gets the identical hard-kill guarantee. Drop a registration with dazai_unregister(pid) when a tool exits cleanly on its own.

End-to-end verification

# terminal 1 — armed daemon
dazai daemon --arm --grace 2

# terminal 2 — MCP server
dazai mcp

# terminal 3 — an MCP client connected to `dazai mcp`:
#   dazai_register(pid = <a process to protect>)
#   dazai_status()  -> { alive: true, armed: true, registered_pids: 1 }

# now kill terminal 1's shell (or drop the heartbeat). Verify:
#   - the daemon is gone (wiped + SIGKILL self)
#   - the registered process is dead (SIGKILL)
#   - dazai_status() from the MCP server now returns { alive: false }

A dead daemon is a valid answer

dazai_status() never errors on a dead daemon — it returns { alive: false }. That is how a client confirms the destruction actually happened.

See the MCP tools reference for every tool's schema.

running on Linux

Linux is where ningen-shikkaku is strongest: it is the only platform where the full hardening set — core-dump suppression, ptrace denial, and the seccomp allowlist — is active. On macOS those three are absent and the daemon says so loudly at startup; mlock and the non-elidable wipe still apply everywhere.

Build dependencies

sudo apt-get install -y libseccomp-dev pkg-config

libseccomp-dev is needed only for the seccomp feature; the default build does not require it.

Build and run with confinement

cd rs
cargo build --release --features seccomp
cargo run --release --features seccomp -- daemon --arm

With the feature on, the daemon installs the kekkai allowlist after setup and before the event loop. A syscall outside the list KillProcess-terminates the daemon.

RLIMIT_MEMLOCK

mlock is bounded by RLIMIT_MEMLOCK. The daemon raises it toward RLIM_INFINITY at startup; if it can't (no CAP_IPC_LOCK, a tight cgroup limit) it warns and continues with best-effort locking.

Grant the capability instead of running as root

Prefer setcap 'cap_ipc_lock=+ep' ./target/release/dazai over running the daemon as root — it gets the lock limit it needs and nothing else.

What's active where

GuaranteeLinuxmacOS
mlock (no swap)
non-elidable wipeexplicit_bzeromemset_s
madvise(MADV_DONTDUMP)
prctl(PR_SET_DUMPABLE, 0)
seccomp allowlist❌ (no-op stub)

Confirm on the real arch

seccomp is validated on aarch64 and x86_64 under the live KillProcess filter. Emulation (Rosetta / qemu-user) cannot install a guest seccomp filter, so it cannot validate the filter — test on native Linux for that architecture, which is exactly what CI does.

configuration

Every flag, by command.

dazai daemon

FlagDefaultMeaning
--armoff (dry-run)enable real self-destruct (wipe + SIGKILL); without it, wipes and logs WOULD but never kills
--grace N5armed graceful-panic grace window, seconds; a reconnect/cancel during it aborts
--ping-timeout N0 (off)panic if no PING arrives within N seconds
--socket PATH${XDG_RUNTIME_DIR:-/tmp}/dazai-$UID.sockUNIX socket path (created 0600)
--exec PATHnonespawn this as a supervised child (sienna); killed first on trigger
--size BYTES4096synthetic working-buffer size

dazai client

FlagDefaultMeaning
--interval N0send PING every N seconds; 0 = just hold the connection open
--socket PATHdaemon defaultsocket to connect to

dazai mcp

FlagDefaultMeaning
--socket PATHdaemon defaultdaemon socket to relay to
--transport stdiostdioMCP transport (stdio is the standard)

motokano

FlagDefaultMeaning
--calls N1exit after N tool calls complete
--sessionoffalso exit when the client disconnects (stdin EOF)
--dazai-socket PATHnoneregister with a daemon and die if it dies
--tool '<spec>'declare a tool (repeatable); see below
--armoff (dry-run)enable the real wipe-and-exit
--grace Ndelay before the final wipe/exit after the trigger

--tool spec

name=<tool name>,kind=static,value=<the secret>     # locked, wipeable
name=<tool name>,kind=exec,cmd=<command + args>     # runs no-shell; stdout NOT locked

Defaults compose

--calls and --session together = exit on either. Adding --dazai-socket adds a third, independent death condition; whichever fires first wins.

socket protocol

The daemon listens on a 0600 UNIX stream socket. A connection is one of two kinds, decided by its first verb.

Heartbeat connection

The first verb is HELLO. This connection becomes the single liveness channel:

HELLO            -> (accepted as the heartbeat; a second concurrent HELLO -> BUSY)
PING             -> PONG        (optional keepalive; required if --ping-timeout > 0)
<disconnect>     -> liveness lost -> panic

One heartbeat at a time

Only one heartbeat client is honored. A second concurrent HELLO is refused with BUSY — the liveness signal can't be ambiguous. Only a reconnect after a real loss cancels an armed grace window.

Control connection

A connection whose first verb is not HELLO is a control connection: request/response, any number of them, never touching the heartbeat lock.

REGISTER pid=<N>    -> OK | BUSY (at 32) | ERROR invalid pid
UNREGISTER pid=<N>  -> OK
ARM                 -> OK (now armed) | ALREADY_ARMED
STATUS              -> STATUS alive=1 armed=<0|1> grace=<n> registered=<n>

Validation and storage

  • PIDs are validated with kill(pid, 0) — they must be > 0 and refer to a live process; 0, negatives, and out-of-range values are rejected before any signal is sent.
  • Registered PIDs are stored in a plain Vec<u32>, capped at 32. They are not secret, so they are not held in a SecretBuffer.

On trigger

Order

On any armed trigger the daemon SIGKILLs every registered PID before wiping its buffers and SIGKILLing itself. In dry-run it only logs WOULD and never kills. See the kill sequence.

The MCP adapter is a client of this protocol: each dazai_* tool maps to one of these verbs, and panics are delivered as SIGUSR1/SIGUSR2 to the PID read from the daemon's <socket>.pid file.

MCP tools

The tools exposed by dazai mcp (rei). Each maps to one verb of the socket protocol or to a panic signal. All run over the standard MCP stdio transport.

dazai_status()

Round-trips STATUS. Never fails — a dead daemon is a valid, expected answer.

{ "alive": true, "armed": true, "grace_seconds": 5, "registered_pids": 1 }

dazai_register(pid)

Registers a PID for session-bound protection (REGISTER pid=<pid>). The PID is SIGKILLed by the daemon on any armed trigger.

{ "ok": true, "message": "registered" }

Returns ok: false if the PID is invalid or the registry is full (32).

dazai_unregister(pid)

Drops a registration (UNREGISTER pid=<pid>).

{ "ok": true, "message": "unregistered" }

dazai_arm()

Arms the daemon at runtime (ARM). Idempotent — arming an already-armed daemon is fine.

{ "armed": true, "message": "armed" }

dazai_panic()

Sends SIGUSR1 to the daemon — a graceful panic that honors the grace window when armed.

{ "triggered": true }

dazai_hard_panic()

Sends SIGUSR2 to the daemon — a hard panic that bypasses the grace window and runs the kill sequence immediately.

{ "triggered": true }

Panics are gated on a live daemon

Before signaling, rei confirms the daemon is alive via STATUS. If the daemon has exited and its PID was recycled by the OS, the panic tools will not signal the recycled PID.

honest limitations

ningen-shikkaku is deliberately honest about where it stops. None of these are bugs — they are the edges of what the mechanism can guarantee, stated up front so you don't over-trust it.

It does NOT protect against

  • GPU VRAM. If an attached agent copies a secret into GPU memory, killing the host process does not wipe VRAM. ningen-shikkaku controls host RAM and the processes it supervises — not an accelerator's memory.
  • exec-tool stdout. motokano's kind=exec tools return a command's stdout, which is OS-buffered and not in a locked buffer. Use kind=static for the wipe guarantee.
  • Managed-runtime residue (Python reference). CPython copies bytes freely, so a secret may transiently live in unlocked heap before/after the locked buffer. The Rust layer eliminates this for its own buffers; the Python tier is a reference, not a guarantee.
  • A privileged or same-UID attacker on the live box (root, /proc/<pid>/mem, a debugger) reading memory before the wipe. mlock stops swap, not reads; seccomp + PR_SET_DUMPABLE raise the bar, but an adversary who already has privileged access to your running machine is out of scope.
  • Cold-boot / DMA / physical attacks on RAM.
  • A hard crash — kernel OOM, an external kill -9, power loss. The wipe path can't run, so nothing is zeroed. The mechanism is best-effort on these paths and never claims otherwise.

Platform gaps

On non-Linux hosts, madvise(MADV_DONTDUMP), prctl(PR_SET_DUMPABLE, 0), and seccomp are absent. mlock and the non-elidable wipe still apply. The daemon prints a loud platform-guarantee notice at startup so this is never silent. See running on Linux.

What's solid

For balance — the guarantees that do hold, and are tested:

  • secrets are not paged to swap (mlock);
  • secrets are wiped with a non-elidable overwrite, on trigger and on Drop;
  • on Linux, secrets are excluded from core dumps and the daemon is not ptrace-able;
  • on Linux, the daemon runs inside a KillProcess seccomp allowlist, validated on both architectures under the live filter;
  • the kill sequence fires exactly once and in a fixed order (kill holders → wipe → self-kill).

Quote

ningen-shikkaku is not, and cannot be, a guarantee against an attacker who already owns your running machine. It is a sharp tool for making secrets ephemeral and session-bound, with every limitation stated up front.