VFS interception lets you inspect and control filesystem operations on mounted guest paths from the host side.
Each rule has:
phase:beforeorafterops: operation filter (create,write,read, etc.)path: filepath-style glob (for example/workspace/*)
Behavior by phase:
before: supports wireaction=block, SDKaction_hookcallbacks, and SDKmutate_hookcallbacksafter: supports SDKhookanddangerous_hookcallbacks
There are two different enforcement paths:
- Host wire rules (
action=block) are sent to the sandbox and evaluated inside host VFS interception. - SDK-local callbacks (
action_hook,mutate_hook,hook,dangerous_hook) run in your SDK process.
For action_hook, matching is based on the SDK API operation:
WriteFile/WriteFileMode->op=writeReadFile->op=readListFiles->op=readdir
So an action_hook with ops=[create] will not match WriteFile.
If you want to block create of a file, use a host wire rule with action=block and ops=[create].
If you want to block SDK write calls directly, use action_hook with ops=[write].
Example:
Rules: []sdk.VFSHookRule{
// Host-side VFS create enforcement.
{
Phase: sdk.VFSHookPhaseBefore,
Ops: []sdk.VFSHookOp{sdk.VFSHookOpCreate},
Path: "/workspace/blocked-create.txt",
Action: sdk.VFSHookActionBlock,
},
// SDK-local write call enforcement.
{
Phase: sdk.VFSHookPhaseBefore,
Ops: []sdk.VFSHookOp{sdk.VFSHookOpWrite},
Path: "/workspace/blocked-write.txt",
ActionHook: func(ctx context.Context, req sdk.VFSActionRequest) sdk.VFSHookAction {
return sdk.VFSHookActionBlock
},
},
}Use typed constants for phases and ops:
sandbox := sdk.New("alpine:latest").WithVFSInterception(&sdk.VFSInterceptionConfig{
Rules: []sdk.VFSHookRule{
{
Phase: sdk.VFSHookPhaseBefore,
Ops: []sdk.VFSHookOp{sdk.VFSHookOpCreate},
Path: "/workspace/blocked.txt",
Action: sdk.VFSHookActionBlock,
},
{
Phase: sdk.VFSHookPhaseBefore,
Ops: []sdk.VFSHookOp{sdk.VFSHookOpWrite},
Path: "/workspace/mutated.txt",
MutateHook: func(ctx context.Context, req sdk.VFSMutateRequest) ([]byte, error) {
return []byte("mutated-by-hook"), nil
},
},
{
Phase: sdk.VFSHookPhaseAfter,
Ops: []sdk.VFSHookOp{sdk.VFSHookOpWrite},
Path: "/workspace/*",
Hook: func(ctx context.Context, event sdk.VFSHookEvent) error {
fmt.Printf("op=%s path=%s size=%d mode=%#o uid=%d gid=%d\n",
event.Op, event.Path, event.Size, event.Mode, event.UID, event.GID)
return nil
},
},
{
Phase: sdk.VFSHookPhaseAfter,
Ops: []sdk.VFSHookOp{sdk.VFSHookOpWrite},
Path: "/workspace/trigger.txt",
DangerousHook: func(ctx context.Context, client *sdk.Client, event sdk.VFSHookEvent) error {
_, err := client.Exec(ctx, "echo hook >> /workspace/hook.log")
return err
},
},
},
})See full runnable examples:
Use exported constants for phases and ops:
from matchlock import (
Sandbox,
VFSInterceptionConfig,
VFSHookRule,
VFSMutateRequest,
VFS_HOOK_ACTION_BLOCK,
VFS_HOOK_PHASE_BEFORE,
VFS_HOOK_PHASE_AFTER,
VFS_HOOK_OP_CREATE,
VFS_HOOK_OP_WRITE,
)
def after_write(event):
print(
f"op={event.op} path={event.path} size={event.size} "
f"mode={oct(event.mode)} uid={event.uid} gid={event.gid}"
)
def dangerous_after_write(client, event):
client.exec("echo hook >> /workspace/hook.log")
def mutate_write(req: VFSMutateRequest) -> bytes:
return b"mutated-by-hook"
sandbox = Sandbox("alpine:latest").with_vfs_interception(
VFSInterceptionConfig(
rules=[
VFSHookRule(
phase=VFS_HOOK_PHASE_BEFORE,
ops=[VFS_HOOK_OP_CREATE],
path="/workspace/blocked.txt",
action=VFS_HOOK_ACTION_BLOCK,
),
VFSHookRule(
phase=VFS_HOOK_PHASE_BEFORE,
ops=[VFS_HOOK_OP_WRITE],
path="/workspace/mutated.txt",
mutate_hook=mutate_write,
),
VFSHookRule(
phase=VFS_HOOK_PHASE_AFTER,
ops=[VFS_HOOK_OP_WRITE],
path="/workspace/*",
hook=after_write,
),
VFSHookRule(
phase=VFS_HOOK_PHASE_AFTER,
ops=[VFS_HOOK_OP_WRITE],
path="/workspace/trigger.txt",
dangerous_hook=dangerous_after_write,
),
],
)
)See full runnable examples:
hookcallbacks areafter-only and run with recursion suppression enabled.dangerous_hookcallbacks areafter-only and bypass recursion suppression.- When SDK after-event callbacks (
hook/dangerous_hook) are present, event emission is enabled automatically for interception.
If you are wiring pkg/vfs directly in host Go code, mutate_write can be dynamic per write:
hooks := vfs.NewHookEngine([]vfs.HookRule{
{
Phase: vfs.HookPhaseBefore,
Ops: []vfs.HookOp{vfs.HookOpWrite},
Action: vfs.HookActionMutateWrite,
MutateWriteFunc: func(ctx context.Context, req vfs.MutateWriteRequest) ([]byte, error) {
// Decide replacement bytes dynamically from metadata.
// req has: path, size, offset, mode, uid, gid.
return []byte("prefix:" + req.Path), nil
},
},
})Notes:
- This is host in-process only (
pkg/vfs/pkg/sandboxintegration), not JSON-RPC payload. - Returning an error from
MutateWriteFuncfails the write.