人間失格
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

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
- 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
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 missingPINGforNseconds → 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.
SIGUSR1→ graceful panic (honors the grace window when armed)SIGUSR2→ hard 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
- raise
RLIMIT_MEMLOCK prctl(PR_SET_DUMPABLE, 0)(Linux)- allocate +
mlockthe secret buffers - spawn the child process (if
--exec) - bind the UNIX socket (
0600) + write the pidfile (0600) - apply seccomp (Linux +
seccompfeature) - 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
- 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--execis killed the same way. - wipe the secret buffers. Each
SecretBufferis overwritten with a non-elidable wipe (explicit_bzero/memset_s/ volatile loop). - remove the pidfile, then
SIGKILLself. 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.
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
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
| Risk | How |
|---|---|
| Secrets paged to swap | mlock pins the buffers into RAM |
| Secrets captured in core dumps | madvise(MADV_DONTDUMP) + prctl(PR_SET_DUMPABLE, 0) (Linux) |
| Session loss leaving secrets resident | heartbeat drop / logout → wipe + SIGKILL |
| Secrets lingering in process memory after use | explicit_bzero / memset_s — a wipe the compiler may not elide |
| A confined process escaping | seccomp allowlist denies execve / open / connect / ptrace / … (Linux) |
| ptrace snooping the daemon | prctl(PR_SET_DUMPABLE, 0) (Linux) |
What 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'skind=exectools return a command's stdout, which is OS-buffered and not held in a locked buffer. Usekind=staticwhen you need the wipe guarantee.- Managed-runtime residue (Python reference). CPython copies
bytesfreely, 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.mlockstops 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
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
KillProcessfilter —aarch64locally andx86_64in CI — where the daemon installs the live filter and the full integration suite runs against it with noSIGSYS. - Every push runs the whole suite (default and
--features seccomp),clippy -D warnings, andrustfmt --checkas 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
| Command | Role |
|---|---|
dazai daemon | the watchdog: holds mlock'd secrets + a UNIX-socket heartbeat, wipes and self-destructs on session loss, seccomp-confined on Linux |
dazai client | the heartbeat client — ties the daemon's life to a shell / SSH session |
dazai mcp | the 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:
- raise
RLIMIT_MEMLOCK prctl(PR_SET_DUMPABLE, 0)(Linux)- allocate +
mlockthe secret buffers (goodnight) - spawn the LLM child if
--exec(sienna) - bind the
0600UNIX socket + write the0600pidfile - apply seccomp (kekkai, Linux +
seccompfeature) - enter the accept/event loop (kikka)
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, mode0600 - pidfile:
<socket>.pid, mode0600 - mode: dry-run unless
--arm - grace:
5seconds (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 dump —
madvise(MADV_DONTDUMP)on Linux; - no silent copies — the type is move-only (no
Clone, noCopy); data is written into the locked mapping through borrow-checked slices, never duplicated onto a GC or general heap; - a wipe on drop —
Dropoverwrites the mapping before unmapping it.
The wipe
The wipe is the whole point, so it is the part the optimizer is forbidden to remove:
| Platform | Mechanism |
|---|---|
| Linux | explicit_bzero |
| macOS | memset_s |
| fallback | volatile 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()— raiseRLIMIT_MEMLOCKtowardRLIM_INFINITYset_process_undumpable()—prctl(PR_SET_DUMPABLE, 0)(disables core dumps +ptrace)current_uid(),pid_exists(),send_signal(),sigkill_pid(),raise_sigkill()
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 —
HELLOmakes 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.
kill_registered → wipe → kill self. Registered holders die before the buffers are wiped; the buffers are wiped before the daemon kills itself.
Single-fire
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
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
| Tier | Behavior |
|---|---|
| core | the syscalls the steady-state loop cannot live without — resolved with ?, so a name that fails to resolve is a hard error |
| runtime | best-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.
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'sCommand(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.
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.
--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--execchild would be.
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
| Tool | Effect | Returns |
|---|---|---|
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.
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
| Kind | Behavior | Wipe guarantee |
|---|---|---|
static | serves a pre-loaded value held in a locked SecretBuffer | yes — wiped on exit |
exec | runs an operator-configured command (no shell) and returns its stdout | no — stdout is OS-buffered, 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:
| Flag | Condition |
|---|---|
--calls N (default 1) | exit after N tool calls complete |
--session | exit when the client disconnects (stdin EOF) |
--dazai-socket PATH | register with a dazai daemon and self-destruct if it dies |
--calls and --session together mean exit on either.
The exit is the product
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.
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
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
- configuration — every flag
- MCP agent integration — protect an agent
- running on Linux — the full hardening set
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.
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.
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.
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 }
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.
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
| Guarantee | Linux | macOS |
|---|---|---|
mlock (no swap) | ✅ | ✅ |
| non-elidable wipe | ✅ explicit_bzero | ✅ memset_s |
madvise(MADV_DONTDUMP) | ✅ | ❌ |
prctl(PR_SET_DUMPABLE, 0) | ✅ | ❌ |
| seccomp allowlist | ✅ | ❌ (no-op stub) |
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
| Flag | Default | Meaning |
|---|---|---|
--arm | off (dry-run) | enable real self-destruct (wipe + SIGKILL); without it, wipes and logs WOULD but never kills |
--grace N | 5 | armed graceful-panic grace window, seconds; a reconnect/cancel during it aborts |
--ping-timeout N | 0 (off) | panic if no PING arrives within N seconds |
--socket PATH | ${XDG_RUNTIME_DIR:-/tmp}/dazai-$UID.sock | UNIX socket path (created 0600) |
--exec PATH | none | spawn this as a supervised child (sienna); killed first on trigger |
--size BYTES | 4096 | synthetic working-buffer size |
dazai client
| Flag | Default | Meaning |
|---|---|---|
--interval N | 0 | send PING every N seconds; 0 = just hold the connection open |
--socket PATH | daemon default | socket to connect to |
dazai mcp
| Flag | Default | Meaning |
|---|---|---|
--socket PATH | daemon default | daemon socket to relay to |
--transport stdio | stdio | MCP transport (stdio is the standard) |
motokano
| Flag | Default | Meaning |
|---|---|---|
--calls N | 1 | exit after N tool calls complete |
--session | off | also exit when the client disconnects (stdin EOF) |
--dazai-socket PATH | none | register with a daemon and die if it dies |
--tool '<spec>' | — | declare a tool (repeatable); see below |
--arm | off (dry-run) | enable the real wipe-and-exit |
--grace N | — | delay 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
--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
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> 0and 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 aSecretBuffer.
On trigger
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 }
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.
- 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'skind=exectools return a command's stdout, which is OS-buffered and not in a locked buffer. Usekind=staticfor the wipe guarantee.- Managed-runtime residue (Python reference). CPython copies
bytesfreely, 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.mlockstops swap, not reads; seccomp +PR_SET_DUMPABLEraise 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
KillProcessseccomp 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).