AI Leak Watch: Build an Import-Time Release Gate for Python AI Packages A developer has published a release-gate approach for Python AI packages that inspects the exact registry artifact, traces a cold import, and plants harmless credentials to catch malicious behavior before any feature is called. The technique responds to Socket's analysis of compromised MemTensor npm and PyPI releases, where simply running `import memos` launched a bundled cross-platform Go binary named `sckit` that searched developer locations for credentials. The gate fails packages that silently start undeclared executables during import, though Socket's findings came from static analysis and no victim count was reported. If your test begins after import , you may already be late. Socket's analysis of the compromised MemTensor packages found that import memos was enough to start a bundled native program. The affected OpenClaw plugin also launched the program when the gateway started and again during memory recall, passing the user's prompt in an environment variable. In plain English, loading the library was itself an execution trigger. The application did not have to call a risky feature first. That changes the test boundary. A normal functional test asks whether the package returns the right result. A supply-chain gate must first ask what the package did before the test called any feature. This article turns that question into a small release gate for Python packages used in AI systems. The gate checks the downloaded artifact, traces a cold import, plants harmless credentials, and fails on behavior the package has no reason to perform. The MemTensor case is serious, but the evidence has limits. Socket reported three malicious npm releases and one malicious PyPI release. Each carried cross-platform Go binaries named sckit . Its researchers found code that started the binaries in the background with the host environment, searched developer locations for credentials, and referenced attacker-controlled infrastructure. Socket also said its findings came from static analysis. It had not executed the samples, had not confirmed how publishing access was obtained, and had not reported a victim count. The OpenClaw plugin passed prompt text to the hidden process. That establishes an unauthorized data path, but it does not prove that every prompt was successfully exfiltrated. Those caveats do not weaken the release decision. A package that silently starts an undeclared executable during import should fail before anyone argues about the payload's final success. Do not run this investigation on a developer laptop or a normal CI runner. Use a disposable environment with no real credentials, no repository write token, and no package-publishing token. Start with the exact registry artifact. Do not inspect only the repository tag. mkdir -p evidence/wheels evidence/npm python -m pip download \ --no-deps \ --only-binary=:all: \ --dest evidence/wheels \ 'your-package==2.4.1' cd evidence/npm npm pack '@your-scope/your-plugin@1.7.0' --ignore-scripts Record the registry URL, version, digest, download time, and the source commit that the release claims to represent. Provenance is useful only when the consumer verifies it against an expected source and builder. A provenance badge is not a substitute for that comparison. Package diffs should be routine. I care most about new native binaries, executable permission changes, new build backends, and abrupt growth in unpacked size. The following script compares a wheel, npm tarball, or source archive with the last approved artifact. The two-times growth threshold is an example policy, not a universal rule. Set it from the normal history of the package. python /usr/bin/env python3 import sys import tarfile import zipfile from dataclasses import dataclass from hashlib import sha256 from pathlib import Path MACH O = { b"\xfe\xed\xfa\xce", b"\xce\xfa\xed\xfe", b"\xfe\xed\xfa\xcf", b"\xcf\xfa\xed\xfe", b"\xca\xfe\xba\xbe", b"\xbe\xba\xfe\xca", } @dataclass frozen=True class FileFacts: size: int digest: str native: bool executable: bool def is native data: bytes - bool: return data.startswith b"\x7fELF" or data.startswith b"MZ" or data :4 in MACH O def facts data: bytes, mode: int - FileFacts: return FileFacts size=len data , digest=sha256 data .hexdigest , native=is native data , executable=bool mode & 0o111 , def inspect archive path: Path - dict str, FileFacts : files: dict str, FileFacts = {} if zipfile.is zipfile path : with zipfile.ZipFile path as archive: for item in archive.infolist : if item.is dir : continue with archive.open item as stream: data = stream.read mode = item.external attr 16 files item.filename = facts data, mode return files if tarfile.is tarfile path : with tarfile.open path as archive: for item in archive.getmembers : if not item.isfile : continue stream = archive.extractfile item data = stream.read if stream else b"" files item.name = facts data, item.mode return files raise ValueError f"Unsupported archive: {path}" baseline = inspect archive Path sys.argv 1 candidate = inspect archive Path sys.argv 2 new files = sorted candidate.keys - baseline.keys changed native = sorted name for name, current in candidate.items if current.native and name not in baseline or baseline name .digest = current.digest new executable = sorted name for name, current in candidate.items if current.executable and name not in baseline or not baseline name .executable old size = sum item.size for item in baseline.values new size = sum item.size for item in candidate.values growth = new size / old size if old size else float "inf" print f"unpacked growth: {growth:.2f}x" print "new files:", new files, sep="\n " failures = if changed native: failures.append f"new or changed native binaries: {changed native}" if new executable: failures.append f"new executable permissions: {new executable}" if growth 2.0: failures.append f"unpacked size grew {growth:.2f}x" if failures: raise SystemExit "RELEASE BLOCKED\n" + "\n".join failures Run it against adjacent approved and candidate versions: python tools/gate artifact.py \ evidence/wheels/your package-2.4.0-py3-none-any.whl \ evidence/wheels/your package-2.4.1-py3-none-any.whl This gate is deliberately narrow. It does not decide that every binary is malicious. It decides that a new or replaced binary requires an owner, source, build record, expected hash, and review before release. The digest comparison matters because an attacker can replace a binary without changing its filename. Static inspection tells you what arrived. A cold-import trace tells you what ran. On a disposable Linux runner, install the candidate into an isolated virtual environment. Give the test a temporary home directory containing fake secrets. Then trace process execution, outbound connection attempts, and file opens while importing the module. bash /usr/bin/env bash set -euo pipefail module=${1:?usage: trace import.sh MODULE} trace dir=$ mktemp -d trap 'rm -rf "$trace dir"' EXIT test home="$trace dir/home" mkdir -p "$test home/.aws" "$test home/.ssh" printf '%s\n' '//registry.npmjs.org/: authToken=CANARY NPM 73f8' \ "$test home/.npmrc" printf '%s\n' ' default ' 'aws access key id=CANARY AWS 4ca1' \ "$test home/.aws/credentials" printf '%s\n' 'CANARY SSH PRIVATE KEY 9b62' \ "$test home/.ssh/id ed25519" env -i \ PATH="$PATH" \ HOME="$test home" \ MODULE="$module" \ API KEY='CANARY API KEY c012' \ timeout 15s strace -ff -qq -s 4096 \ -e trace=execve,connect,openat \ -o "$trace dir/import" \ .venv/bin/python -I -c \ 'import importlib, os; importlib.import module os.environ "MODULE" ' python tools/assert import trace.py "$trace dir" "$test home" The assertion script treats any non-Python child executable, internet socket, or read of the planted files as a release failure: python /usr/bin/env python3 import re import sys from pathlib import Path trace dir = Path sys.argv 1 test home = str Path sys.argv 2 lines = for path in trace dir.glob "import " : lines.extend path.read text errors="replace" .splitlines exec paths = for line in lines: match = re.search r'execve\ " ^" + "', line if match: exec paths.append Path match.group 1 .name allowed = {"python", "python3", "python3.13"} unexpected exec = name for name in exec paths if name not in allowed internet connect = line for line in lines if "connect " in line and "AF INET" in line or "AF INET6" in line secret reads = line for line in lines if "openat " in line and test home in line and any name in line for name in ".npmrc", "credentials", "id ed25519" failures = { "unexpected executables": unexpected exec, "internet connection attempts": internet connect, "canary credential reads": secret reads, } failures = {name: values for name, values in failures.items if values} if failures: for name, values in failures.items : print f"{name}:" for value in values: print f" {value}" raise SystemExit "RELEASE BLOCKED" The test does not need a successful connection. An undeclared attempt is enough to stop the release. Run the same probe with the network disabled as a containment control, but keep the trace. A blocked call is still evidence about intent and behavior. strace is Linux-specific. On macOS, use an isolated VM with Endpoint Security telemetry or an equivalent process and network monitor. On Windows, use a disposable VM with Process Monitor and network capture. Python introspection alone is not a universal integrity control because native code and detached processes can run outside the interpreter's view. Import isn't the only window. An AI package also receives real data during memory recall, tool calls, telemetry flushes, and error handling. Any of those paths can carry a canary value out. Run one black-box scenario for each lifecycle hook. Use a different canary string for every path. Capture subprocess environments, files, logs, DNS, and HTTP. The assertion is simple: the canary may reach only the destinations named in the design. For a memory plugin, I would test at least these events: Do not use one canary everywhere. Distinct values tell you which hook created the unexpected path. Define the decision before the pipeline fails. I would start with this matrix and tune only the package-size threshold from the project's release history. | Check | Expected result | Release blocker | |---|---|---| | Artifact provenance | Registry artifact ties to the approved source and builder | Missing or mismatched source, builder, or digest | | Artifact diff | Binary hashes, executable bits, and build files match the approved change | New or changed native binary, executable bit, or build hook without approval | | Unpacked size | Change stays within the package's normal range | Unexplained growth beyond the project threshold | | Cold import | Import starts only the expected interpreter | Any undeclared child process | | Credential canaries | Package ignores credentials outside its documented scope | Read of a planted token, key, or credential file | | Lifecycle canaries | Prompt and memory values reach only approved components | Canary appears in an undeclared process, file, log, or endpoint | | Network trace | Only documented destinations are contacted | Undeclared DNS lookup or connection attempt | Do not average these signals into a score. A package does not become acceptable because six checks passed after it launched an unexplained binary. Under the OWASP GenAI LLM Top 10 for 2026 https://genai.owasp.org/resource/owasp-genai-llm-top-10-2026/ , the primary mapping is LLM04:2026 Supply Chain . The likely impact also maps to LLM02:2026 Sensitive Information Disclosure because the code targeted credentials and exposed prompt text to an untrusted process. I would not map this primarily to prompt injection or excessive agency. The model did not need to make a bad decision. The package executed before the model mattered. A dependency scanner can tell you that a version is known to be bad. It cannot prove that the artifact matches the source you reviewed, explain why a wheel grew twenty times larger, or show that an import started a detached process. The approach is the same across all three gates: treat each phase as a trust boundary, define the expected behavior before the test runs, and block on any deviation. Installation, import, startup, and the AI lifecycle hooks are all surfaces where a bad package can act. A gate that covers one but not the others just makes the attacker pick a different surface. If the package runs code before your first test step, that behavior belongs inside the test plan. My course, AI Security Testing: LLM-03 Supply Chain Testing https://www.udemy.com/course/ai-security-testing-llm-03-supply-chain-testing/ , covers practical ways to verify third-party components, release artifacts, and dependency behavior. The course title follows the 2025 OWASP numbering. Supply Chain moved to LLM04 in the 2026 list. View all of my AI security testing courses https://www.udemy.com/user/jonathan-fisher-69/ .