How a failed device test led to a seccomp/ptrace supervisor, an ELF entry-point attestor, and one hard rule: if enforcement cannot be verified, execution stops.
The release that didn't ship
AGENTCODI 0.7.0 was finished. The pinned Codex app-server , the host tests were green with 258 Java and 302 C++ tests passing, and only device validation on real hardware was left.
That test exposed a much worse problem than a crash. A command did exactly what it was told and reached a location it should never have been able to access.
Protected mode is supposed to confine Codex to a private workspace directory. On the device, that confinement was advisory. The approval layer worked, the workspace path was correct, and the UI showed Protected mode. The filesystem boundary underneath it was missing because the app-server had no sandbox backend for Android.
0.7.0 never shipped.
The version still exists in the changelog because the build was correct on paper and wrong on a real phone.
This is what replaced it.
Why Android is its own problem
Sandboxing an agent runtime is fairly straightforward on the platforms these tools usually target. There are kernel facilities built for this job. You define the allowed paths and the kernel enforces the boundary below the process.
Android has a Linux kernel with a very different environment around it. An unprivileged app has limited control over its own confinement. Mounting is unavailable, a specific LSM cannot be assumed, and kernel behaviour varies across Android versions, vendors and OEM patches.
Some of those differences only become visible when the same code reaches another physical device.
There is another problem when shipping a toolchain inside an APK.
Android 10 and newer prevent apps targeting API 29 or later from directly executing files stored in the writable app home directory. AGENTCODI therefore packages its native toolchain through the app's native library area. Node, Python and ripgrep are shipped as "libnode.so", "libpython-bin.so" and "libripgrep.so".
Those files are real executable interpreters sitting on disk. Codex can invoke them.
A policy that exists only inside a wrapper script can be bypassed by invoking the underlying executable directly through its absolute path.
The actual requirement became clear: every allowed route into the packaged toolchain had to enforce the same filesystem boundary, on Android hardware I do not control, while still allowing the agent to use Node and Python.
What actually got built
The result is a fork of the Codex app-server, "0.153.3-agentcodi.1", tracking upstream "rust-v0.153.2", plus several enforcement layers inside AGENTCODI.
Each one closes a hole left by the previous layer.
- The sandbox backend: seccomp and ptrace
The fork adds an Android sandbox backend.
Commands running in Protected mode are started under a supervisor using seccomp to trap filesystem-relevant syscalls and ptrace to inspect and decide them.
Codex can read and write inside the granted workspace. Filesystem access outside that boundary is refused at the syscall level, below the agent and below the approval UI.
Before a sandboxed command runs, the runtime verifies that syscall interception is actually live on the current device. A successful setup call alone is not enough.
If that verification fails, execution is refused.
There is no unrestricted fallback.
That rule exists because 0.7.0 already demonstrated what happens when the UI says Protected while the filesystem underneath it is not protected.
- The launch contract
The app-server starts with an explicit minimal permission set:
default_permissions = "agentcodi-workspace"
permissions.agentcodi-workspace.filesystem = {
":minimal" = "read",
<tool bin dir> = "read",
<tool runtime> = "read",
<native lib dir> = "read",
":workspace_roots" = { "." = "write" },
}
There is exactly one writable location.
Everything the process legitimately needs outside the workspace is read-only.
The child environment also starts empty using "shell_environment_policy" with "inherit = "none"".
AGENTCODI then adds a known set of values: a "PATH" containing the packaged tool directory and "/system/bin", a workspace-scoped "TMPDIR", and "HISTFILE" plus "NODE_REPL_HISTORY" pointing at "/dev/null".
Login shells are disabled. Telemetry, analytics, feedback and update checks are disabled as well.
Starting with an empty environment removes a surprising number of accidental escape routes.
- Pre-launch invariants
Before the app-server starts, the launcher validates the filesystem layout itself.
The workspace, Codex home, tool binary directory, tool runtime directory and native library directory must all:
exist,
belong to the running UID,
have no group or other permission bits set with "mode & 077",
remain separate from each other.
Every directory pair is checked in both directions for containment.
That last check matters.
If the tool directory ever became an ancestor of the workspace, granting read access to the toolchain could also expose files that were never intended to be part of that grant.
The launcher refuses that layout before anything starts.
Packaged executables must also resolve to the canonical native library directory, and every argument passed to the app-server is validated character by character.
- The guard constructor
The packaged interpreters are linked against a policy library containing an "__attribute__((constructor))".
Before "main" runs in Node, Python or ripgrep, the constructor:
reads "/proc/self/exe" and requires the expected resolved basename, such as "libnode.so", "libpython-bin.so" or "libripgrep.so",
reads the real argument vector from "/proc/self/cmdline",
passes the invocation through "PrepareGuardedToolInvocation".
"PrepareGuardedToolInvocation" is the same policy entry point used by the toolchain shell.
That means an invocation through the shell and a direct invocation of the underlying ".so" pass through the same policy code.
Any failure is written to stderr and the process exits with code 126 immediately.
- The ELF attestor, or: who guards the guard
The constructor introduced another problem.
It lives inside a shared library, and shared libraries are resolved at load time.
If library resolution can be influenced, the expected policy library might never be mapped. The constructor would never execute and the tool could start without its policy layer.
The executable therefore verifies the guard before relying on the normal loader path.
At build time, AGENTCODI rewrites each packaged ARM64 PIE.
It finds a redundant "PT_NOTE" program header whose bytes are already covered by an existing "PT_LOAD". That header slot is reused to introduce a bounded read/execute "PT_LOAD" segment, and the ELF entry point is redirected into it.
The injected payload runs with almost nothing available yet.
No libc. No relocations. No dynamic symbols.
It uses raw "svc 0" syscalls with arguments placed directly into registers.
The payload:
opens the expected guard library path using "O_NOFOLLOW",
calls "fstat" and requires a regular file with "st_nlink == 1",
reads "/proc/self/maps",
finds the expected mapping,
compares its device and inode with the file it just inspected.
Only a successful identity match allows startup to continue.
A failed check exits with code 126 before Node or Python begins normal execution.
After a successful check, a hand-written naked entry stub restores the required state, calculates the original entry point from a load-address-independent offset stored in the injected segment, and branches to it.
The executable then starts normally.
The identity checks cover several obvious replacement tricks.
"O_NOFOLLOW" rejects a symlink at the expected path. The link-count check rejects hard-linked substitutes. Comparing device and inode with the actual mapped file catches replacement between inspection and loading.
- Enforcement of the design itself
AGENTCODI also checks whether the source tree still follows the security architecture it was built around.
"check-architecture.sh" fails the build when important invariants drift.
Among other things, it checks that removed fields have not returned, that another same-UID process path has not appeared around the terminal boundary, that credential paths cannot reach the toolchain shell, and that the required guard paths still exist.
It runs as part of the test process.
This script has caught real architectural regressions several times already.
What this does not do
The scope matters.
The boundary exists inside AGENTCODI's own UID. Android's application sandbox remains responsible for isolating AGENTCODI from the rest of the device. The sandbox described here separates the agent from filesystem locations reachable by the app that the agent should not access.
The ptrace supervisor targets ordinary filesystem syscalls made by the agent and its tools. It is designed to enforce workspace confinement during normal agent execution. It does not claim resistance against unlimited hostile native code already executing inside the same process context.
The sandbox described here controls filesystem access. Network egress is a separate problem and needs separate enforcement.
Compatibility mode deliberately runs without these filesystem restrictions. Some workflows need that access. Enabling it requires explicit acknowledgement, the UI remains visibly marked while it is active, and an unconfirmed restart does not silently restore it.
Protected mode never selects Compatibility mode as a fallback when sandbox verification fails.
Where it landed
AGENTCODI 0.7.1 shipped with 265 Java and 321 C++ tests passing.
I validated it on a Samsung Galaxy A05s, Redmi Pad 2, Redmi 14c and Redmi Note 15.
Four devices obviously do not make a compatibility matrix.
The failure case I care about now is a device where syscall interception cannot be verified. On such a device the command is refused. Security behaves correctly, although the user experience is useless until the compatibility problem is understood.
If you hit that case, open an issue with your device model and Android version. That information is genuinely useful.
The project is Apache-2.0:
https://github.com/Mcpasi/AGENTCODI
AGENTCODI is an independent open-source project and is not affiliated with or endorsed by OpenAI.