Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.celesto.ai/llms.txt

Use this file to discover all available pages before exploring further.

Overview

Callbacks let you hook into the SmolVM command lifecycle. You subclass Callback, override the hooks you care about, and pass instances to SmolVM(callbacks=[...]). Every other hook is a no-op by default. For a task-oriented walkthrough, see Run callbacks and safety hooks.

Callback

Base class for SmolVM command-lifecycle callbacks. Subclass it and override only the hooks you need.
from smolvm import Callback

class MyCallback(Callback):
    def on_pre_run(self, ctx): ...
    def on_post_run(self, ctx): ...
    def on_run_error(self, ctx): ...

Hooks

Each hook receives a single RunContext argument.

on_pre_run

def on_pre_run(self, ctx: RunContext) -> None
Called before a command is sent to the guest. This is the veto channel — if it raises, the command is aborted and the exception propagates to the caller of run(). Raise CommandBlockedError for an explicit, typed block. A blocked command keeps the SSH or vsock connection open, so the next allowed run() call reuses it.

on_post_run

def on_post_run(self, ctx: RunContext) -> None
Called after a command completes successfully. ctx.result is populated. Observer hook — exceptions are logged and swallowed so a faulty observer cannot break a command that already ran.

on_run_error

def on_run_error(self, ctx: RunContext) -> None
Called when the transport raised while executing a command. ctx.error is populated. Observer hook — exceptions are logged and swallowed, and the original transport error still propagates from run().

RunContext

Dataclass passed to every hook for a single SmolVM.run() call. Using one object means new fields can be added later without changing any callback’s method signature.
@dataclass
class RunContext:
    vm_id: str
    command: str
    shell: str
    timeout: int
    result: CommandResult | None = None
    error: Exception | None = None
vm_id
str
The VM the command targets.
command
str
The shell command as passed to run().
shell
str
Execution mode — "login" or "raw".
timeout
int
Per-command timeout in seconds.
result
CommandResult | None
default:"None"
The command result. None until on_post_run. See CommandResult.
error
Exception | None
default:"None"
The transport error raised during execution. None unless the hook is on_run_error.

CommandBlockedError

Exception type for vetoing a command from on_pre_run. Inherits from SmolVMError.
class CommandBlockedError(SmolVMError):
    def __init__(
        self,
        message: str,
        vm_id: str | None = None,
        command: str | None = None,
    ) -> None: ...
message
str
required
Human-readable reason for the block.
vm_id
str | None
default:"None"
ID of the VM the command targeted. Stored on the exception and in details.
command
str | None
default:"None"
The blocked command string. Stored on the exception and in details.
Attributes:
  • vm_id (str | None): The VM the command targeted.
  • command (str | None): The blocked command string.
  • message (str): Reason passed to the constructor.
  • details (dict): Contains vm_id and command.

Example

A pre-run hook that blocks a few known-dangerous commands, plus a post-run hook that logs every command:
from smolvm import SmolVM, Callback, CommandBlockedError

class SafetyGuard(Callback):
    DENY = ("rm -rf /", "mkfs", ":(){ :|:& };:")

    def on_pre_run(self, ctx):
        if any(bad in ctx.command for bad in self.DENY):
            raise CommandBlockedError(
                f"Blocked unsafe command: {ctx.command!r}",
                vm_id=ctx.vm_id,
                command=ctx.command,
            )

class AuditLog(Callback):
    def on_post_run(self, ctx):
        print(f"[{ctx.vm_id}] {ctx.command!r} -> exit={ctx.result.exit_code}")

with SmolVM(callbacks=[SafetyGuard(), AuditLog()]) as vm:
    vm.run("echo hello")
    try:
        vm.run("rm -rf /")
    except CommandBlockedError as e:
        print(f"refused: {e.command}")
Callbacks fire in the order they were registered.
Last modified on June 2, 2026