There’s a security problem hiding in plain sight across the AI agent ecosystem, and almost nobody is talking about it.
If your AI agent can read and write files - and it can, that’s the whole point - then every interpreted app it runs is a target for self-modification. The agent can rewrite the code it executes. So can any attacker who compromises the agent through prompt injection.
This isn’t theoretical. It’s a direct consequence of combining AI file system access with interpreted code execution. I ran into this while building Nebo, and it changed how I think about the entire app platform.
The Setup
Most AI agent platforms let third-party apps or plugins extend the agent’s capabilities. The agent runs these apps as part of its workflow. So far, so good.
Now think about what happens when those apps are written in an interpreted language like Python, Node.js, or Ruby.
The app’s source code sits on disk as plaintext. The agent has file system access. It can read index.js. It can write index.js. It can change what the app does without anyone knowing.
Interpreted App:
+------------------------------+
| app/ |
| +-- index.js <-- Agent can read, modify, inject code
| +-- package.json |
| +-- node_modules/ |
| +-- ... <-- Thousands of modifiable JS files
+------------------------------+ A Node.js app with node_modules/ gives the agent thousands of modifiable JavaScript files. A Python app with its .py files and .pyc bytecode (trivially decompilable) is just as exposed. Ruby, PHP, Perl, shell scripts - same story.
The Threat Model
An AI agent - or an attacker who compromises it through prompt injection - could:
- Rewrite the app’s logic. Change what the app does without the user knowing.
- Inject backdoors. Add data exfiltration, credential theft, or remote access code.
- Bypass permission checks. Strip out the app’s own security validations.
- Escalate privileges. Modify the app to request capabilities it wasn’t granted.
- Persist across restarts. The modified source code survives reboots because the changes live on disk.
This isn’t a buffer overflow. It’s not a race condition. It’s the normal, expected behavior of an AI agent doing exactly what it was designed to do - reading and writing files - applied to its own runtime.
And the attack surface scales with the codebase. A single node_modules/ directory can contain hundreds of thousands of lines of modifiable code across thousands of files. Every one of them is a potential injection point.
Why This Is Different From Traditional Sandboxing
Traditional app sandboxing (Docker, iOS, Android) focuses on preventing apps from accessing unauthorized resources. The threat is the app reaching out to things it shouldn’t touch.
AI agent platforms face a different threat: the agent reaching in to modify the apps it runs. The agent isn’t the app. It’s the orchestrator. It has broader access than any single app, and it can use that access to change the apps themselves.
Sandboxing the app’s outbound access doesn’t help when the threat is inbound modification from the agent.
The Solution: Compiled-Only Binaries
A compiled native binary (ELF on Linux, Mach-O on macOS, PE on Windows) is opaque to the agent:
- It can’t meaningfully read or understand compiled machine code
- Random byte modifications to a compiled binary crash it instead of changing behavior
- The logic isn’t accessible as modifiable source text
- Cryptographic signatures catch any tampering
- What was signed is what runs
Compiled App:
+------------------------------+
| app/ |
| +-- binary <-- Opaque machine code, signed, verified
| +-- manifest.json |
| +-- signatures.json |
+------------------------------+ This is the policy I enforce in Nebo. Only compiled native binaries are allowed. Apps written in interpreted or scripted languages get rejected. Non-negotiable.
Supported vs. Rejected Languages
Any language that compiles to a native binary with no runtime interpreter dependency works:
| Language | Status | Notes |
|---|---|---|
| Go | Recommended | Static binary, cross-compiles easily |
| Rust | Supported | Static binary, excellent security properties |
| C / C++ | Supported | Native binary, requires careful memory management |
| Zig | Supported | Native binary, C-interop, cross-compilation |
Anything that needs an interpreter, VM, or runtime to execute gets rejected:
| Language | Why It’s Rejected |
|---|---|
| Node.js / JavaScript | Source code is plaintext .js files - fully readable and modifiable |
| Python | Source code is plaintext .py files; .pyc bytecode is trivially decompilable |
| Ruby | Source code is plaintext .rb files |
| PHP | Source code is plaintext .php files |
| Shell scripts | Source code is plaintext; direct command injection vector |
| Java / Kotlin (JVM) | .jar files contain decompilable bytecode; requires JVM runtime |
| .NET / C# | .dll assemblies contain decompilable IL; requires runtime |
What About Wrapper Tools?
Tools like PyInstaller, Node.js SEA (Single Executable Applications), and GraalVM native-image exist to package interpreted code as executables. Most of them aren’t real compilation.
PyInstaller bundles a complete Python interpreter with your bytecode. At runtime, it extracts everything to a temp directory and runs the interpreter. The source is still there - just zipped inside the binary. We detect this by scanning for _PYINSTALLER strings, libpython dynamic links, and _MEI temp directory markers.
Node.js SEA embeds the V8 engine. Same problem - it’s a compiled wrapper around an interpreted runtime. Detected via libnode link analysis.
GraalVM native-image is the exception. It actually compiles Java/Kotlin to true native machine code with no interpreter. No JVM, no bytecode, no decompilation risk. It passes validation.
The rule is simple: if the binary needs an interpreter to run your code, it’s rejected. If it compiles your code to native machine code, it’s accepted.
Two-Tier Enforcement
I enforce the compiled-only policy at two independent tiers:
Tier 1: NeboLoop (deep analysis, once per upload). Before any app gets signed and distributed through the NeboLoop marketplace, it passes through:
- Magic byte verification - validates ELF, Mach-O, or PE headers
- Hidden interpreter detection - scans for PyInstaller, CPython, Node.js markers
- Dynamic link analysis - inspects shared library dependencies for
libpython,libnode,libjvm,libmono,libruby - Go build info verification - for Go binaries, confirms the Go toolchain built it
- Dropper pattern detection - flags binaries that extract files to
/tmpat runtime - ED25519 signing - only clean binaries get signed
Tier 2: Nebo (fast verification, every launch). The local client runs lightweight checks because signed binaries already passed deep validation:
- Magic byte check
- Shebang rejection (no
#!/usr/bin/pythonscripts) - ED25519 signature verification
- File permission enforcement (binary installed as read+execute only, no write)
Same pattern as mobile app stores. Apple and Google do deep analysis at submission time. Your phone does fast signature checks at install and launch. Thorough security without slowing down launch time.
W^X: The OS Has Your Back
Modern operating systems enforce Write XOR Execute (W^X) - memory pages can’t be simultaneously writable and executable. This is OS-level protection that adds yet another layer:
macOS requires Hardened Runtime and code signing. Apple Silicon enforces W^X in hardware via Pointer Authentication Codes (PAC).
Linux uses mprotect W^X enforcement. SELinux and AppArmor add distribution-level policies that deny execmem.
Windows has DEP (Data Execution Prevention) and ASLR. Modern Windows requires signed binaries for protected processes.
Even in a worst-case scenario where a buffer overflow in a compiled app gets exploited, the attacker can’t write shellcode to memory and execute it (W^X blocks this), can’t write a new binary to disk and run it (signature verification blocks this), and can’t modify the running binary’s code in memory (code pages are read+execute only).
The Embedded Interpreter Exception
A compiled binary that internally embeds a scripting engine (say, a Go binary that embeds Lua) is fine. The outer binary is still compiled, signed, and opaque. The embedded scripts live inside the binary, not as modifiable files on disk.
This works because:
- The agent can’t modify the embedded scripts (they’re baked into the compiled binary)
- The signature covers the entire artifact including embedded resources
- The sandbox prevents the process from modifying its own binary
The distinction is between code-as-data (embedded, immutable, signed) and code-as-files (sitting on disk, readable, writable). The first is safe. The second isn’t.
Why This Matters Now
The AI agent ecosystem is growing fast. Platforms are shipping plugin systems and app marketplaces. Most of them accept interpreted code without thinking about what it means to run that code alongside an AI agent with file system access.
Every Node.js plugin. Every Python extension. Every shell script hook. They’re all writable by the agent that runs them.
This isn’t a vulnerability in any specific piece of code. It’s a category of risk that comes from the architecture itself. And the fix is straightforward: compile your apps, sign them, and verify them on every launch.
The full security architecture with all seven layers of defense is at nebo/security.
LinkedIn: linkedin.com/in/almatuck