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 dataThese 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.setprofileEvent names usually follow the operation or module that triggers them.
Some are broad:
open
compile
exec
importSome are more specific:
subprocess.Popen
socket.connect
ctypes.dlopen
os.systemA 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 CPythonIf 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 hookThis 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.denied51.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 possibleUse operating system isolation for untrusted code:
separate process
container
VM
seccomp or pledge-like controls where available
filesystem permissions
user namespaces
resource limits
network isolationAudit 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 jsonImport audit events can help detect:
unexpected dependencies
dynamic imports
plugin loading
native extension loading
module shadowing
policy violationsBlocking 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
execA 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.setprofileThese 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 state51.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 runtimesThe 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 executionFor 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 carefully51.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 = FalseKeep 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 eventversus:
hook 1 blocks event
hook 2 never sees eventFor 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 = argsunless 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 execPoor policy:
block anything suspicious
block all imports
raise on every event
inspect all arguments deeplyAudit 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 codeA 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 payloads51.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 dataAudit 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 flowAudit 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 pathsExact 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 = replacementunless 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 authorsGood:
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 subprocessesHook 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 itAt the Python level:
sys.audit(event, *args)
calls registered hooks
raises if a hook raisesThe 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.
| Mechanism | Purpose |
|---|---|
| Audit hooks | Observe security-sensitive runtime events |
| Tracing | Observe line execution, calls, returns, exceptions |
| Profiling | Observe function calls and returns with lower detail |
| Logging | Application-defined messages |
| Monitoring | Broader 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 behaviorA 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 protocolAudit hooks can add visibility and deny some Python-level operations inside that sandboxed process.
51.45 Common Bugs
| Bug | Cause | Fix |
|---|---|---|
| Hook misses early events | Hook installed too late | Install native hook before initialization |
| Hook recurses | Hook logs or imports | Keep hook minimal, add guard |
| Program breaks on import | File/import policy too broad | Allow runtime and stdlib paths |
| Policy bypassed by extension | Native code bypasses Python API | Use OS isolation, block native loading |
| Test pollution | Hook cannot be removed | Run audit-hook tests in subprocess |
| Slow runtime | Hook handles every event heavily | Filter early and avoid expensive work |
| Fragile argument parsing | Assumes wrong event shape | Check 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 allowIt 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 moduleA 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.