Skip to content

51. Audit Hooks

sys.addaudithook, the cpython.PyAudit_AddHook C API, and auditable events across the standard library.

Audit hooks are CPython’s runtime mechanism for observing security-sensitive and operationally important events. They let embedders, security tools, test harnesses, and monitoring systems receive notifications when Python code performs actions such as importing modules, opening files, starting subprocesses, compiling code, executing dynamic code, loading native extensions, or changing tracing state.

Audit hooks do not replace operating system sandboxing. They are an observability and policy-enforcement mechanism inside the Python runtime.

At the Python level, audit hooks are exposed through sys.addaudithook.

import sys

def hook(event, args):
    print(event, args)

sys.addaudithook(hook)

open("example.txt", "w")

When audited operations occur, CPython calls registered hooks with an event name and an argument tuple.

51.1 Why Audit Hooks Exist

Python programs can perform many dynamic operations.

Examples:

import code by name
load extension modules
open files
create sockets
start subprocesses
compile source strings
execute dynamic code
inspect stack frames
set trace functions
modify import paths
deserialize data

These operations are powerful. They also matter for security, debugging, compliance, and embedding.

An embedding host may want to know when embedded Python code opens a file.

A security monitor may want to block subprocess creation.

A test harness may want to detect dynamic code execution.

Audit hooks provide one runtime-level place to observe such events.

51.2 Basic Audit Hook

A hook is a callable with this shape:

def hook(event, args):
    ...

Register it:

import sys

def audit_hook(event, args):
    print("event:", event)
    print("args:", args)

sys.addaudithook(audit_hook)

After registration, the hook receives audit events.

Example:

import sys

def hook(event, args):
    if event.startswith("open"):
        print(event, args)

sys.addaudithook(hook)

with open("data.txt", "w") as f:
    f.write("hello")

Output shape:

open ('data.txt', 'w', 524865)

The exact argument values depend on the event and CPython version.

51.3 Event Names

Audit events are identified by string names.

Examples include:

open
import
compile
exec
eval
subprocess.Popen
socket.connect
os.chdir
os.remove
os.rename
ctypes.dlopen
sys.settrace
sys.setprofile

Event names usually follow the operation or module that triggers them.

Some are broad:

open
compile
exec
import

Some are more specific:

subprocess.Popen
socket.connect
ctypes.dlopen
os.system

A hook can filter by prefix or exact event name.

def hook(event, args):
    if event == "subprocess.Popen":
        ...

51.4 Event Arguments

The args parameter is a tuple.

def hook(event, args):
    print(type(args))

The meaning of each tuple element depends on the event.

For example, an open event may include the path, mode, and flags.

A subprocess.Popen event may include executable information and arguments.

A hook should be defensive because events differ.

def hook(event, args):
    if event == "open":
        path = args[0] if args else None
        print("opening", path)

Audit hook code should not assume all events have the same shape.

51.5 Hooks Are Process-Local Runtime Observers

A Python audit hook observes events inside the current Python process.

It does not observe:

other processes
kernel-level activity outside Python
native code that bypasses Python APIs
external programs after they start
operating system activity unrelated to CPython

If a C extension calls operating system APIs directly without emitting audit events, a Python-level hook may not see those operations.

Audit hooks improve visibility inside CPython. They are not a complete system security monitor.

51.6 Adding Hooks

Register hooks with:

sys.addaudithook(hook)

Hooks are called in registration order.

import sys

def first(event, args):
    print("first", event)

def second(event, args):
    print("second", event)

sys.addaudithook(first)
sys.addaudithook(second)

open("x.txt", "w")

Both hooks receive the event.

Once added, a Python-level audit hook cannot be removed through the public Python API.

This is deliberate. Audit hooks are intended to be hard for application code to silently disable.

51.7 Native Audit Hooks

Embedding applications can install native audit hooks from C before Python code runs.

This matters because Python-level hooks are installed after Python has already started executing.

A native embedding hook can observe earlier runtime events and is harder for Python code to bypass.

Conceptually:

host application starts
host installs native audit hook
CPython initializes
Python code runs
audit events flow to native hook

This is the strongest use case for audit hooks: embedding hosts that need policy or monitoring before user Python code executes.

51.8 Emitting Audit Events

Python code can manually emit audit events with:

sys.audit(event, *args)

Example:

import sys

def dangerous_operation(path):
    sys.audit("myapp.dangerous_operation", path)
    return open(path).read()

A hook receives:

event = "myapp.dangerous_operation"
args = ("path-value",)

Custom audit events are useful for frameworks and embedded systems.

Use namespaced event names to avoid collisions:

myapp.plugin.load
myapp.tenant.start
myapp.policy.denied

51.9 Blocking Operations

An audit hook can block an operation by raising an exception.

import sys

def hook(event, args):
    if event == "subprocess.Popen":
        raise RuntimeError("subprocesses are disabled")

sys.addaudithook(hook)

Now code that tries to create a subprocess may fail.

import subprocess

subprocess.Popen(["echo", "hello"])

The audit hook exception interrupts the audited operation.

This makes audit hooks useful for policy enforcement, but enforcement is only as strong as the coverage of audited operations and the trustworthiness of code running in the process.

51.10 Hook Exceptions

If a hook raises an exception, the audited operation usually fails.

Example:

def hook(event, args):
    if event == "open":
        raise PermissionError("file access blocked")

This can prevent file opening through Python’s normal open path.

Be careful. A hook that raises too broadly can break the interpreter, imports, logging, cleanup, or standard library internals.

Bad:

def hook(event, args):
    raise RuntimeError("block everything")

This can make the process unusable.

Policy hooks should be precise.

51.11 Audit Hooks Are Not Sandboxes

Audit hooks cannot safely sandbox arbitrary untrusted Python code by themselves.

Reasons:

coverage is runtime-specific and API-specific
native extensions may bypass Python-level events
the same process memory is shared
malicious code may exploit bugs
hooks run inside the process they monitor
operating system resources are still shared
denial of service remains possible

Use operating system isolation for untrusted code:

separate process
container
VM
seccomp or pledge-like controls where available
filesystem permissions
user namespaces
resource limits
network isolation

Audit hooks are useful inside a defense-in-depth design. They are not the boundary.

51.12 Import Auditing

Imports are audited.

A hook can observe imports:

import sys

def hook(event, args):
    if event == "import":
        print("import:", args)

sys.addaudithook(hook)

import json

Import audit events can help detect:

unexpected dependencies
dynamic imports
plugin loading
native extension loading
module shadowing
policy violations

Blocking imports can be tricky because the import system itself needs many modules to function.

A safe policy usually blocks specific modules rather than broad import behavior.

def hook(event, args):
    if event == "import":
        name = args[0]
        if name == "subprocess":
            raise ImportError("subprocess disabled")

Even this may not prevent all subprocess use if the module was already imported or if native code bypasses it.

51.13 File Access Auditing

File operations are common audit targets.

def hook(event, args):
    if event == "open":
        path = args[0]
        print("open", path)

A policy can restrict paths:

import os
import sys

allowed_root = os.path.abspath("/safe/root")

def hook(event, args):
    if event == "open":
        path = args[0]

        if isinstance(path, str):
            full = os.path.abspath(path)
            if not full.startswith(allowed_root + os.sep):
                raise PermissionError(full)

sys.addaudithook(hook)

This is useful for monitoring, but not a complete filesystem sandbox. Race conditions, symlinks, file descriptors, native code, and alternate APIs can complicate enforcement.

51.14 Subprocess Auditing

Subprocess creation is security-sensitive.

import sys

def hook(event, args):
    if event == "subprocess.Popen":
        print("process:", args)

sys.addaudithook(hook)

A policy can block subprocesses:

def hook(event, args):
    if event == "subprocess.Popen":
        raise PermissionError("subprocess disabled")

This prevents normal Python subprocess creation paths.

It does not prevent native code from calling OS process APIs directly if such native code is already loaded and able to do so.

51.15 Dynamic Code Auditing

Compilation and execution are audited.

Examples:

code = compile("x = 1", "<dynamic>", "exec")
exec(code)

Events may include:

compile
exec

A hook can observe these:

def hook(event, args):
    if event in {"compile", "exec"}:
        print(event, args)

Dynamic code execution matters for security and debugging because it can obscure where behavior comes from.

Blocking all compile or exec events may break legitimate tools, import machinery, templates, dataclasses, typing machinery, and frameworks.

51.16 Tracing and Profiling Auditing

Changing trace or profile functions is audited.

Relevant operations include:

sys.settrace
sys.setprofile

These matter because tracing and profiling can inspect execution, frames, locals, and call flow.

A host may want to prevent untrusted code from installing trace hooks.

def hook(event, args):
    if event in {"sys.settrace", "sys.setprofile"}:
        raise PermissionError(event)

This is useful in controlled environments, but still not a complete sandbox.

51.17 Socket Auditing

Network operations can emit audit events.

A hook may observe connection attempts.

def hook(event, args):
    if event.startswith("socket."):
        print(event, args)

This can help monitor outbound connections.

A policy can restrict connections:

def hook(event, args):
    if event == "socket.connect":
        sock, address = args
        host, port = address
        if port != 443:
            raise PermissionError("only HTTPS allowed")

Network policy is better enforced at the OS, container, or firewall layer. Audit hooks can add Python-level visibility.

51.18 ctypes and Native Loading

Loading native libraries is security-sensitive.

Audit events can observe operations such as dynamic library loading through ctypes.

def hook(event, args):
    if event.startswith("ctypes."):
        print(event, args)

Native library loading can bypass many Python-level restrictions because native code can call OS APIs directly.

A restricted environment should usually block ctypes, extension loading, and other native escape hatches at multiple layers.

Audit hooks can detect or block some of these attempts.

51.19 Audit Hooks and Extension Modules

C extensions can emit audit events using CPython’s C API.

Conceptually:

PySys_Audit("module.operation", "O", object);

This lets native modules participate in the audit system.

Extension authors should emit audit events for operations that are security-sensitive or operationally important.

Examples:

opening external resources
loading native libraries
starting processes
connecting to networks
executing user-provided code
changing global process state

51.20 Audit Hooks and Embedding

Embedding applications are a major target use case.

A host application can install a native audit hook before running user scripts.

Use cases:

application scripting
plugin runtimes
database embedded Python
game engines
automation systems
scientific workflow hosts
controlled notebook runtimes

The host can observe or deny operations according to policy.

Example policies:

no subprocesses
only read files under a workspace
no native library loading
no network access
no trace hook installation
log all imports
log dynamic code execution

For strong isolation, combine this with process-level controls.

51.21 Audit Hooks and Logging

A hook can log audit events.

import sys

def hook(event, args):
    if event in {"open", "subprocess.Popen", "socket.connect"}:
        print("audit", event, args)

sys.addaudithook(hook)

Be careful using logging inside hooks.

Logging may trigger audited operations, such as file writes, imports, lock acquisition, or formatting code.

A hook can accidentally create recursion.

Safer approaches:

write to a pre-opened low-level stream
filter aggressively
avoid imports inside the hook
avoid heavy formatting
avoid calling unknown code
buffer events carefully

51.22 Recursion in Audit Hooks

Audit hooks can trigger more audit events.

Example:

def hook(event, args):
    print(event, args)

Printing may touch streams. Stream operations may trigger lower-level events in some contexts.

A hook should avoid complex side effects.

Use a recursion guard if needed:

import sys
import threading

local = threading.local()

def hook(event, args):
    if getattr(local, "inside", False):
        return

    local.inside = True
    try:
        if event == "subprocess.Popen":
            sys.stderr.write(f"blocked: {event}\n")
            raise PermissionError(event)
    finally:
        local.inside = False

Keep hooks short and predictable.

51.23 Performance Cost

Audit hooks run during audited operations.

A hook that does expensive work can slow the whole program.

Bad:

def hook(event, args):
    analyze_entire_stack()
    write_to_database()
    import_large_module()

Better:

def hook(event, args):
    if event not in watched_events:
        return
    record_small_event(event, args)

Design hooks with the same care as logging in hot paths.

51.24 Hook Ordering

Hooks run in the order they were added.

If one hook raises an exception, later hooks may not run for that event.

This matters when combining logging and enforcement.

Example:

hook 1 logs event
hook 2 blocks event

versus:

hook 1 blocks event
hook 2 never sees event

For embedding hosts, install critical hooks as early as possible.

51.25 Audit Events Before Python Hooks

A Python-level hook is added only after Python code calls sys.addaudithook.

Events before that call are missed by that hook.

Example:

import os
import sys

def hook(event, args):
    print(event, args)

sys.addaudithook(hook)

The import of os happened before the hook was installed.

Native hooks installed by the embedding host can observe earlier events.

This is important for security-sensitive hosts.

51.26 Inspecting Audit Events

A simple exploratory hook:

import sys

def hook(event, args):
    if event.startswith(("open", "import", "subprocess", "socket", "ctypes")):
        print(event, args)

sys.addaudithook(hook)

Run a small program and observe what events occur.

This is useful for learning, but avoid broad printing in production because it can be noisy and recursive.

51.27 Event Stability

Audit event names and argument structures are part of a documented interface, but code should still be defensive across Python versions.

Good hook design:

def hook(event, args):
    if event == "open":
        if not args:
            return
        path = args[0]
        ...

Avoid fragile assumptions:

path, mode, flags, extra = args

unless the event contract guarantees that shape for the versions you support.

51.28 Policy Design

A policy hook should answer precise questions.

Good policy:

block subprocess.Popen
block ctypes.dlopen
block open outside /workspace
block socket.connect except approved hosts
log compile and exec

Poor policy:

block anything suspicious
block all imports
raise on every event
inspect all arguments deeply

Audit hooks work best with narrow, explicit rules.

51.29 Allowlist Example

import os
import sys

allowed_root = os.path.abspath("workspace")

def inside_allowed_root(path):
    full = os.path.abspath(path)
    return full == allowed_root or full.startswith(allowed_root + os.sep)

def hook(event, args):
    if event == "open":
        path = args[0]

        if isinstance(path, str) and not inside_allowed_root(path):
            raise PermissionError(path)

    if event == "subprocess.Popen":
        raise PermissionError("subprocess disabled")

    if event.startswith("ctypes."):
        raise PermissionError("native loading disabled")

sys.addaudithook(hook)

This is a useful teaching example. In production, path policy needs to handle symlinks, file descriptors, race conditions, platform paths, and native bypasses.

51.30 Audit Hooks and Tests

Tests can use audit hooks to detect forbidden behavior.

Example:

import sys

events = []

def hook(event, args):
    if event in {"subprocess.Popen", "socket.connect"}:
        events.append((event, args))

sys.addaudithook(hook)

At the end of a test, assert no forbidden event occurred.

Because hooks cannot be removed, test suites must be careful. A hook installed in one test can affect later tests in the same process.

Often, audit-hook tests should run in a subprocess.

51.31 Audit Hooks and Import-Time Behavior

If a hook blocks file access or imports, it may affect imports themselves.

Importing Python modules can require:

opening source files
opening bytecode files
reading directories
loading extension modules
executing module code

A file policy that blocks too much can break imports.

A practical policy may allow Python’s own library paths and restrict application data paths separately.

51.32 Audit Hooks and eval

Dynamic evaluation is audited.

eval("1 + 2")

A hook can watch for this:

def hook(event, args):
    if event in {"compile", "exec"}:
        print(event, args)

eval compiles and executes code. Depending on the path, both compilation and execution events may be relevant.

Blocking dynamic execution can break libraries that legitimately generate code.

Use policy based on context, not only the event name, when possible.

51.33 Audit Hooks and Serialization

Some serialization mechanisms can execute code or load classes dynamically.

Audit hooks may observe imports, code execution, or other side effects caused during deserialization.

However, audit hooks do not make unsafe deserialization safe.

Dangerous deserialization should be avoided or sandboxed externally.

For untrusted data, prefer safe formats and parsers.

JSON over pickle
strict schemas
no arbitrary object loading
no dynamic imports from untrusted payloads

51.34 Audit Hooks and pickle

pickle can construct arbitrary Python objects and can be dangerous with untrusted input.

Audit hooks may observe some operations caused by pickle loading, but they are not a complete defense.

The primary rule remains:

do not unpickle untrusted data

Audit hooks can help detect or restrict some dangerous behavior, but they do not transform pickle into a safe format.

51.35 Audit Hooks and Frame Access

Accessing frames and trace machinery can expose sensitive information.

Examples:

locals
globals
call stack
function arguments
execution flow

Audit events around tracing, profiling, or frame access can help hosts detect introspection.

A policy may restrict:

sys.settrace
sys.setprofile
frame inspection APIs
debugger attachment paths

Exact coverage depends on the operation.

51.36 Audit Hooks and Monkey Patching

Audit hooks observe events emitted by CPython and participating libraries. They do not automatically prevent all monkey patching.

Python code can still rebind names:

module.function = replacement

unless the program or host prevents it by other means.

Audit hooks are strongest around audited runtime operations, not general object mutation.

If mutation control matters, use object capability design, restricted namespaces, separate processes, or custom wrappers.

51.37 Audit Hooks and sys.audit

sys.audit itself raises an audit event.

sys.audit("app.event", 1, 2, 3)

Hooks receive:

event = "app.event"
args = (1, 2, 3)

If any hook raises, sys.audit raises.

This means custom application audit events can also be enforceable.

Example:

def delete_user(user_id):
    sys.audit("app.user.delete", user_id)
    perform_delete(user_id)

A hook can block deletion.

51.38 Designing Custom Events

Good custom audit events are:

stable
namespaced
low-cardinality in event name
clear in argument shape
emitted before the sensitive operation
documented for policy authors

Good:

sys.audit("myapp.plugin.load", plugin_name, path)

Poor:

sys.audit(f"myapp.plugin.load.{plugin_name}")

Keep dynamic data in arguments, not in the event name.

51.39 Before vs After Events

For policy enforcement, emit before the operation.

sys.audit("myapp.file.delete", path)
os.remove(path)

If a hook raises, deletion does not happen.

For logging completion, use a separate after-event if useful.

sys.audit("myapp.file.delete", path)
os.remove(path)
sys.audit("myapp.file.deleted", path)

Most security checks should occur before the effect.

51.40 Audit Hook Safety Rules

Hook code should:

avoid imports
avoid complex logging
avoid acquiring locks that may already be held
avoid calling untrusted code
avoid broad exceptions
filter early
keep argument inspection simple
fail closed only for precise policies
be tested in subprocesses

Hook code runs inside sensitive runtime paths. Treat it like signal handler or allocator-adjacent code: small, predictable, and conservative.

51.41 CPython Internals Mental Model

At the C level, an audited operation calls into CPython’s audit machinery.

Conceptually:

operation wants to proceed
    call PySys_Audit(event, format, ...)
        build argument tuple
        call registered native hooks
        call registered Python hooks
        if hook raises, propagate exception
    continue operation if no hook blocked it

At the Python level:

sys.audit(event, *args)
    calls registered hooks
    raises if a hook raises

The operation decides where to place the audit call. For enforcement, it should be placed before the sensitive action.

51.42 Relationship to Tracing and Profiling

Audit hooks differ from tracing and profiling.

MechanismPurpose
Audit hooksObserve security-sensitive runtime events
TracingObserve line execution, calls, returns, exceptions
ProfilingObserve function calls and returns with lower detail
LoggingApplication-defined messages
MonitoringBroader operational telemetry

Audit hooks are event-oriented. They are not a general instruction-by-instruction tracing system.

51.43 Relationship to Import Hooks

Import hooks customize how modules are found and loaded.

Audit hooks observe that import-related events occur.

They solve different problems.

import hook:
    changes import behavior

audit hook:
    observes or blocks audited import behavior

A custom importer should emit audit events for sensitive behavior, especially if it loads code from unusual locations.

51.44 Relationship to Sandboxing

Sandboxing requires controlling resources.

Audit hooks can participate in sandbox-like policy enforcement, but they do not provide complete isolation.

A real sandbox usually needs:

process boundary
filesystem restrictions
network restrictions
resource limits
native code restrictions
environment isolation
limited IPC
strict input/output protocol

Audit hooks can add visibility and deny some Python-level operations inside that sandboxed process.

51.45 Common Bugs

BugCauseFix
Hook misses early eventsHook installed too lateInstall native hook before initialization
Hook recursesHook logs or importsKeep hook minimal, add guard
Program breaks on importFile/import policy too broadAllow runtime and stdlib paths
Policy bypassed by extensionNative code bypasses Python APIUse OS isolation, block native loading
Test pollutionHook cannot be removedRun audit-hook tests in subprocess
Slow runtimeHook handles every event heavilyFilter early and avoid expensive work
Fragile argument parsingAssumes wrong event shapeCheck event docs and unpack defensively

51.46 Minimal Audit Policy Example

import os
import sys

workspace = os.path.realpath("workspace")

def is_inside_workspace(path):
    try:
        full = os.path.realpath(path)
    except TypeError:
        return True

    return full == workspace or full.startswith(workspace + os.sep)

def audit_policy(event, args):
    if event == "open":
        path = args[0] if args else None
        if isinstance(path, str) and not is_inside_workspace(path):
            raise PermissionError(path)

    elif event == "subprocess.Popen":
        raise PermissionError("subprocesses are disabled")

    elif event.startswith("ctypes."):
        raise PermissionError("ctypes is disabled")

sys.addaudithook(audit_policy)

This demonstrates the structure of a policy hook:

filter event
extract relevant arguments
decide allow or deny
raise to block
return to allow

It is not a complete sandbox.

51.47 Minimal Custom Audit Events

Application code can expose policy points.

import sys

def load_plugin(name, path):
    sys.audit("app.plugin.load", name, path)

    module = load_plugin_from_path(name, path)

    sys.audit("app.plugin.loaded", name, path)
    return module

A host can block plugin loading:

def hook(event, args):
    if event == "app.plugin.load":
        name, path = args
        if name not in allowed_plugins:
            raise PermissionError(name)

This makes application-level sensitive operations visible to the same audit system as CPython-level operations.

51.48 Design Rules

Install hooks as early as possible.

Use native hooks for embedding or security-sensitive hosts.

Filter by exact event names or narrow prefixes.

Raise exceptions only for precise policy violations.

Keep hook code small.

Avoid imports, complex logging, and lock-heavy behavior inside hooks.

Treat audit hooks as observability and policy hooks, not as standalone isolation.

Emit custom audit events before application-level sensitive operations.

Run tests involving global hooks in subprocesses.

51.49 Minimal Mental Model

Use this model:

CPython emits audit events around sensitive operations.

Each event has a name and argument tuple.

Registered hooks receive the event.

If a hook returns normally, the operation continues.

If a hook raises, the operation usually fails.

Python-level hooks are process-local and cannot be removed.

Native hooks can be installed earlier by embedding hosts.

Audit hooks improve visibility and policy enforcement, but they are not a complete sandbox.

51.50 Key Points

Audit hooks let CPython and applications report sensitive runtime events.

Use sys.addaudithook to register Python-level hooks.

Use sys.audit to emit custom events.

Hooks receive an event name and argument tuple.

Hooks can block operations by raising exceptions.

Audit hooks are useful for embedding, monitoring, policy enforcement, testing, and security visibility.

They do not replace operating system isolation.

Hook code must be small, careful, and defensive.

For strong control, combine audit hooks with process isolation, filesystem policy, network policy, native-code restrictions, and resource limits.