Skip to content

Repository files navigation

mkclaw: Claw learns the UNIX way!

Simple, single-binary AI agent inspired by OpenClaw. Talk to your claw over Signal, and have it carry out advanced computing and coding tasks without a public webhook.

mkclaw has host-managed tools:

  • Execute: run scripts inside the guest VM, optionally with approved host-held secrets injected as env vars.
  • RestartVM: request a governed guest VM restart when health checks fail.
  • Subagent: spawn a fresh-context subagent thread and get a UUID plus transcript path.
  • WaitForSubagentFinish: wait for specific subagent IDs and return structured results/pending IDs.
  • ContinueSubagent: append a follow-up message to an existing subagent conversation.
  • RequestNewSecret: ask the Signal owner to add or update a host-held secret value.

The guest VM is an OCI-based micro-VM powered by krunvm.

All other features are built around these tools:

  • Crons: Markdown files that periodically schedule agent tasks.
  • Hooks: .md files or hashbang scripts run before each user or cron run, with their output creating the LLM's system prompt.

All of your crons/hooks live in the VM's /root directory and are shared with your system. For a starting point, I recommend most users fork their root from mkclaw-vanilla: the "default distribution" of mkclaw, which uses crons/hooks to add support for Agent Skills, Web Browsing, Web Search, and Memory.

Warning

Security Notice

mkclaw is an LLM that runs arbitrary code inside a guest VM.

We take multiple steps to isolate the environment: a micro-VM, and a guest/host model where AI provider keys are not directly exposed to the guest.

However, IT IS YOUR RESPONSIBILITY to limit mkclaw access to sensitive information, such as API keys, private data, credit cards, logins, etc.

See: The Lethal Trifecta

Layout

root/
  hooks/
    0001_example.md
    0002_example.py
  crons/
    0001_example.md
  threads/
    <thread-uuid>.jsonl
  threads-archive/
    <legacy-thread-id>-<timestamp>.jsonl

Config

See mkclaw.example.yaml for a starting point.

  • root points to the host directory mounted as /root in the guest.
  • ai.provider must be openai-responses.
  • ai.base_url points at an OpenAI-compatible API root (default https://api.openai.com/v1).
  • ai.default_profile and ai.profiles select model/reasoning combinations; subagents may choose a configured profile.
  • ai.compaction enables token-budget transcript compaction through /root/compaction.md.
  • channel.signal configures Signal polling through signal-cli.
  • secrets is the host-held env-var vault available to Execute after approval.
  • secret_approvals stores durable cron-scoped approvals for secret use.
  • vm.image is the OCI image booted by krunvm (default ghcr.io/openai/codex-universal:latest).
  • vm.name is the krunvm VM name (default mkclaw).
  • vm.cpus is the number of vCPUs (default: host parallelism).
  • vm.mem is guest memory in MiB (default: 4096).
  • vm.vm_dir stores VM artifacts.

Signal channel

mkclaw uses signal-cli for Signal send/receive. This avoids public webhooks: the host periodically runs signal-cli receive --json --timeout ... and sends replies with signal-cli send.

Signal does not provide an official bot API. This setup is best treated as a personal/low-volume channel. You must install and configure signal-cli before starting mkclaw.

Example setup:

# Register a dedicated Signal number, or link signal-cli as a secondary device.
# Exact commands vary by signal-cli version; see signal-cli's documentation.
signal-cli -a +15551230000 register
signal-cli -a +15551230000 verify CODE

# Quick smoke test.
signal-cli -a +15551230000 send -m "hello" +15551234567
signal-cli -a +15551230000 receive --json --timeout 5

Config:

channel:
  signal:
    account: "+15551230000"
    allowed_numbers:
      - "+15551234567"
    signal_cli_path: signal-cli
    poll_interval_seconds: 5
    receive_timeout_seconds: 5

Only numbers in allowed_numbers can enqueue agent work.

Secrets vault

Secrets are host-held env var values. They live in top-level secrets: in mkclaw.yaml and are never shown to the model:

secrets:
  GITHUB_TOKEN: "ghp_..."

secret_approvals: []

The agent can see only the secret names. When a command needs one, it calls Execute with a secrets list:

{
  "text": "#!/usr/bin/env bash\ngh pr create --title '...' --body '...'",
  "timeout": 300,
  "secrets": ["GITHUB_TOKEN"]
}

mkclaw sends a Signal approval prompt to the first number in channel.signal.allowed_numbers before injecting those env vars into that one process:

Secret use requested

Secrets: [GITHUB_TOKEN]
Command hash: <sha256>

Reply:
1 approve once
2 approve this exact command + secret set for 10 minutes
3 approve this exact command + secret set for 1 hour
no reject

Cron-originated requests also include:

4 approve this secret set + command hash for this cron revision

Option 4 persists a durable approval under secret_approvals; it is tied to the secret names, the exact Execute command hash, the cron file path, and the cron content revision. Editing the cron changes the revision and requires a new approval.

If the agent needs a missing secret, it calls RequestNewSecret with env var names and a reason. You approve by replying with values:

approve <approval-id>
GITHUB_TOKEN=ghp_...

To reject:

no <approval-id>

Approved secret values are patched into the secrets: block in mkclaw.yaml with file mode 0600 on Unix. mkclaw intentionally does not use yaml-edit for this path because that library can corrupt inline empty YAML blocks during mutation.

Security tradeoff: scripts run inside the VM, so a compromised guest could still exfiltrate a secret during an approved process lifetime. The vault reduces exposure from "credentials always present in the VM" to "credentials present only for one approved process".

Guest daemon

Thread state is stored in the guest under /root/threads/*.jsonl as native append-only mkclaw JSONL events. Runtime state is derived by reading the log; there is no file-size compare-and-swap token and no pending-inbound sidecar. If guestd is about to write a native thread over an older non-native .jsonl thread file, it first moves the legacy file to /root/threads-archive/.

The guest daemon (mkclaw-guestd.py) is embedded in the mkclaw binary and sent to a tiny python3 -c bootloader over the krunvm child stdin stream. It is not mounted as a persistent guest file, so updating mkclaw updates guestd on the next guestd restart without recreating the VM.

The guest image must include python3. Guestd uses a netstring-framed JSON RPC protocol over krunvm stdin/stdout; guest networking is not part of the mkclaw control plane.

VM lifecycle

On first run, mkclaw will:

  1. Ensure a local containers policy exists at vm.vm_dir/containers-home/.config/containers/policy.json
  2. Reuse an existing krunvm VM with the configured name, or krunvm create a new VM from the OCI image with the root directory mounted as /root
  3. krunvm start the VM with piped stdio, executing a small Python bootloader as the entry command
  4. Send the embedded guestd source over stdin and wait for a stdio health response

Workload networking is handled transparently by krunvm's TSI (Transparent Socket Impersonation) — no TAP device or host-side routing needed. The mkclaw control plane uses krunvm stdio instead of guest networking. The OCI rootfs is read-write via buildah, so the guest can install packages and modify system files freely.

Required host tools: krunvm, buildah, and signal-cli.

Optional: build with --features systemd to inhibit system sleep while mkclaw is running. This uses the systemd-inhibit CLI and requires it to be available in PATH.

mkclaw uses buildah unshare krunvm ... with HOME pointed at vm.vm_dir/containers-home, so it can use its own .config/containers/policy.json without requiring a global /etc/containers/policy.json.

Quickstart

# 1) Install krunvm, buildah, and signal-cli
# See https://github.com/containers/krunvm and https://github.com/AsamK/signal-cli

# 2) Configure mkclaw.yaml
cp mkclaw.example.yaml mkclaw.yaml

# 3) Start mkclaw
mkclaw

NixOS dev shell

nix develop

AI providers

mkclaw uses the OpenAI-compatible Responses protocol directly:

  • openai-responses: uses POST /v1/responses with Responses-style function tools.

Requests send store: false.

Hooks

Hooks are executed in lexicographic order before each user or cron run. The stdout from each hook is appended to the system prompt with a blank line between entries.

Supported hook types:

  • Markdown (.md): included directly in the system prompt.
  • Hashbang scripts (first line starts with #!): executed and their stdout is included.

Crons

Cron files are markdown with frontmatter:

---
name: "Example"
cron: "0 */6 * * * * *"
---
Do something every 6 hours.

Cron schedules are interpreted in UTC.

About

Claw learns the UNIX way!

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages