ALPS 0.1: agent signing isolation
Start at the level that needs no sandbox setup. Add observable, enforceable isolation as your repository risk grows.
Choose an ALPS level
Agentic Levels for Provenance and Signing is a usability–assurance ladder, not a pass/fail grade. Choose a level for the repository’s risk and environment. Each level is cumulative, reduces reliance on model behavior, and preserves the same in-toto evidence and policy semantics.
Requirements are cumulative: each row includes every requirement above it.
| Level | Minimum requirements | Verifiable evidence | Focus |
|---|---|---|---|
| ALPS 0 · Guided | Agent/repository guidance and CI/lock evidence for the exact command, result, and commit. | Signed statement bytes, subjects, command/result, certificate, and trusted time; no independently verified signer separation. | Quick adoption and accidental misuse. |
| ALPS 1 · Authenticated | Platform-issued tenant, repository, purpose, audience, and time-bound identity; mandatory RFC 3161 timestamp. | Authenticated workload issuance and ephemeral signing event bound to the evidence. | Impersonation, replay, and stale-proof ambiguity. |
| ALPS 2 · Constrained | Enforced sandbox plus trusted observations of the measured agent, exact CI/lock executable, applied policy, process, environment, mounts, and egress. | Signed boundary facts showing the agent lacked named session, key, and socket paths within the observer’s coverage. | Direct credential access and common escape paths. |
| ALPS 3 · Isolated | Signing service outside the agent boundary; typed requests; measured/signed CI/lock identity; hardware-backed or remote-protected authority. | Independent request re-derivation, service identity, measured binary, and non-exportability evidence. | Compromised or adversarial agents. |
Hermeticity modifiers
Hermeticity is an orthogonal evidence facet. A high ALPS level does not imply a closed execution, and a lower ALPS level can receive a hermeticity modifier when trusted observations support it.
| Modifier | Open | Constrained | Complete | Evidence required |
|---|---|---|---|---|
| Hermeticity | H-Open Unrestricted external runtime influence is permitted and recorded. | H-Constrained External access is technically restricted to an explicit allowlist; destinations and relevant inputs are observed. | H-Complete All inputs are staged and content-identified; network is disabled or confined entirely within the independently measured boundary; no unmeasured external influence crosses it. | Signed facts covering the assessed boundary identity and scope, material inputs, ambient environment and descriptors, filesystem roots, toolchain, cache, network policy and outcomes, and explicit host inputs. |
ALPS-0 / H-OpenALPS-2 / H-ConstrainedALPS-3 / H-Complete
Hermeticity: Unknown receives no H modifier when evidence is missing or the observer’s boundary coverage is insufficient. Unknown is not treated as open.
ALPS 0.1 borrows its cumulative page structure from the SLSA v1.2 Build track. ALPS 0.1 measures agent-to-signer provenance and isolation; it is not a SLSA level, replacement, certification, or equivalence claim. SLSA Build L3 does not require hermeticity, and its hermetic-build discussion treats closed dependencies as a distinct assurance. The H-* modifiers are defined by ALPS 0.1, not SLSA.
Under ALPS 0.1, the producer does not self-assert a level. A verifier derives the highest supported level from signed evidence. A configuration file proves configured intent only; it becomes enforcement evidence when a trusted observer outside the agent records that exact policy digest as applied to the measured process. Missing evidence means Unknown, never safe by assumption.
Intent not independently proved.
Within the observer’s stated coverage.
External enforcement with signed measurement.
Cryptography and required bindings validated.
Never promoted to safe by assumption.
Where an assessed ALPS level would be consumed
Pushgate verifies signed evidence, identity, policy, and the platform decision at git push. It does not read, derive, or require an ALPS level, and no shipped verifier does. A Supported ALPS cell in the support matrix means the evidence that level needs can be produced and checked with a CI/lock policy today; it does not mean a verifier assigns the level. This section is the consumption contract an assessor would have to satisfy first, written down so an integration is designed against it rather than around it.
Git push is Pushgate’s first consumption-time enforcement point, and it is not a limit of ALPS: the same contract would let a verifier take independently assessed ALPS evidence as one policy input before granting any sensitive action — running a deployment, publishing an artifact, invoking a privileged API, changing infrastructure, or releasing access to a protected service.
Each authorization would remain narrow. A consumer must verify the ALPS attestation and bind its decision to the exact requested action, target, tenant, workload, inputs, purpose, and time window. An ALPS level is evidence for policy; it is never a reusable bearer credential or blanket permission to execute commands.
| Enforcement point | Example exact bindings |
|---|---|
| Git admission | Repository, commit, ref update, policy, and push nonce. |
| CI/CD deployment | Workflow, immutable artifact digest, environment, deployment command, and release approval. |
| Privileged API access | API audience, operation, resource, tenant, purpose, and short expiry. |
| Artifact publication | Registry, package identity, artifact digest, provenance policy, and destination. |
ALPS 0 · Guided
Mermaid source
flowchart LR
R[Agent rules] --> A[Coding agent]
A -->|invokes| C[CI/lock]
C --> E[Signed evidence]
N[Cooperative guidance] -. same process authority .-> ASummary
CI/lock records and signs the exact command, result, subjects, and commit while repository rules tell the agent how to use it.
Intended for
Teams that want a first signed proof from cooperative, safety-aligned coding agents without setting up a sandbox first.
Requirements
Add the same rule to AGENTS.md, CLAUDE.md, and a project Cursor rule. Keep the real home, platform session, SSH agent, signer caches, cloud credentials, and signer sockets out of the agent environment.
# Agent proof rule
- Run each policy-bearing command through the approved, version-pinned CI/lock binary.
- Use repository setup commands exactly; do not add signer or platform flags.
- Never inspect or use platform sessions, signing authority, SSH_AUTH_SOCK,
signer caches, cloud credentials, or signer sockets.
- Report a CI/lock refusal to the human; never bypass it.
Evidence a verifier checks
Exact evidence bytes and subjects, observed command and result, signer certificate, commit binding, and RFC 3161 signing time. Instruction-file digests may be recorded as declared context.
Benefits and mitigations
Reduces accidental or casual credential misuse and makes the required proof workflow explicit.
Limits
There is no independently verified process separation or proof that credentials were absent. A prompt-injected or malicious same-user agent can ignore prose, read available files, environment, or sockets, select another binary, or run another allowed command.
Install and verify CI/lock
Install the supported release into a host-owned path outside the workspace. Pin the approved version and verify the installer against its published SHA-256 before execution; the installer then verifies the release archive against the same published manifest before installing it. This checks integrity, not provenance — to verify who built the binary, use the release-policy guide below with a CI/lock you already trust.
# ALPS 0.1 TRUSTED BOOTSTRAP — the first thing this block does, before any command runs.
# PATH is pinned to the base system, so every utility below (env, find, mkdir, mktemp, ...)
# is the host's own and never this repository's. The PATH you had is kept aside for two
# uses only: LOCATING the third-party tools this block then verifies, and handing a
# verified tool the PATH its own helpers expect. A second block pasted into the same
# shell keeps the PATH the first one saved, rather than saving the pin as if it were yours.
PUSHGATE_AMBIENT_PATH="${PUSHGATE_AMBIENT_PATH:-$PATH}"
export PATH='/usr/bin:/bin'
# SAME-UID LIMIT — what the checks below do, and what they cannot do.
# pushgate_host_tool refuses a tool supplied from inside this workspace, and refuses one whose
# file or parent directory is world-writable. That catches a repository-controlled PATH entry and
# a binary any OTHER local user could have replaced. It does NOT establish that the tool is
# genuine. Someone already running as YOUR user can write $HOME/bin/nono or $HOME/bin/gh with
# ordinary 0755 permissions: outside the workspace, not world-writable, so it passes every check
# here and is then invoked as the "verified" tool. A fake 'gh attestation verify' exits 0 and a
# fake 'nono' prints the expected version, and the agent is recorded as sandboxed and launched
# with no sandbox at all. This applies to every tool resolved below, cosign, curl and cilock
# included, and to the attestation check that verifies nono.
# No file-permission test can close this: the check runs with exactly the privileges of the
# process it would have to catch. The mitigation is a different privilege domain, not a stronger
# probe — run the agent under a SEPARATE UID, in a container, or in a VM, so that writing
# $HOME/bin is not something it can do in the first place.
#
# GROUP-WRITE, separately, IS yours to close. These checks test the other-write bit and not the
# group-write bit, and Homebrew's default prefix is group-writable: measured on Apple silicon,
# /opt/homebrew/bin is 'drwxrwxr-x <user>:admin' (Intel /usr/local/bin is the same). So any other
# member of that group can replace nono, gh, cosign or cilock before confinement starts and these
# checks still report host-owned. That is a different principal from you, unlike the case above.
# It is not rejected automatically because rejecting it refuses every Homebrew-installed tool,
# which is the install path this guide recommends. If anyone else has admin on this machine,
# close it yourself: chmod g-w /opt/homebrew/bin, or install these tools outside a group-writable
# prefix.
#
# OWNERSHIP ASSUMPTION, and its cost. A resolved tool is accepted only when YOU or ROOT own it,
# because any other owner can rewrite those bytes whenever they like. The cost lands on honest
# setups: a tool owned by a SERVICE ACCOUNT -- a CI runner toolchain under a build user, a
# linuxbrew prefix, a vendored toolchain installed as its own daemon user -- is refused here even
# though nobody is attacking. If that is your machine, it is a stated limit of this check rather than
# a finding: install or resolve the tool from a path owned by you or by root. It is deliberately
# not widened by guessing at a system-uid threshold, because macOS numbers its first human user
# 501 and Linux reserves under 1000 for daemons, so any single threshold would either trust a
# human peer on macOS or refuse a service account on Linux.
pushgate_host_has() { (PATH="$PUSHGATE_AMBIENT_PATH"; command -v "$1") >/dev/null 2>&1; }
pushgate_world_writable() {
__wwl="$(ls -ld "$1" 2>/dev/null)" || return 2
set -- $__wwl
[ $# -ge 3 ] || return 2
__wwo="$3"
__wwl="$1"
case "$__wwl" in ??????????*) ;; *) return 2 ;; esac
# WHO THIS PROCESS IS, resolved BEFORE any branch may answer "trusted".
# Every acceptance below is a statement about the owner, so an acceptance
# reached without this value is an acceptance made without asking the
# question. Recomputed on every call rather than cached across the walk:
# caching would let a __wwme already set in the pasted shell's environment
# decide who this process is, which hands the same-UID attacker a fresh lever
# to buy back a few forks the walk would never have noticed.
__wwme="$(id -un 2>/dev/null)" || return 2
[ -n "$__wwme" ] || return 2
# A STICKY directory is world-writable but restricted-deletion: anyone may
# create their own entries, and only the owner of an existing entry may
# unlink or rename it. That is precisely the question here — can someone
# else swap these bytes — so /tmp and friends are NOT replaceable. Without
# this, the ancestor walk below rejects every tool under /tmp on Linux,
# where TMPDIR is /tmp (drwxrwxrwt); macOS hides the bug because TMPDIR is a
# private per-user /var/folders path. CI caught exactly that difference.
#
# RESTRICTED-DELETION DOES NOT BIND THE DIRECTORY'S OWN OWNER. Sticky stops a
# STRANGER from unlinking somebody else's entry; the owner of the directory
# may still rename or remove anything inside it. So the sticky bit answers
# "can a stranger swap this" and says nothing about the owner, and a 1777
# directory belonging to another principal can replace the verified staging
# subtree between the integrity check and the bash that runs it. That is
# this file's own defect one branch further in — a mode bit accepted as an
# answer to a question about ownership — so the sticky branch asks the same
# question every other acceptance asks instead of returning early.
case "$__wwl" in d????????[tT]*)
case "$__wwo" in "$__wwme"|'root') return 1 ;; *) return 0 ;; esac ;;
esac
__wwl="${__wwl#????????}"
case "$__wwl" in w*) return 0 ;; -*) ;; *) return 2 ;; esac
# WHO OWNS IT, not just who else can write it. A 0755 file has its other-write
# bit clear and is still rewritable at will BY ITS OWNER, so a tool owned by
# another local user was being accepted as host-owned on the strength of a bit
# that says nothing about them. Same defect as checking a directory without its
# ancestors: the thing was validated and its context was not.
#
# Only this user and root qualify. Root because the base system is not
# installed by the reader; this user because that is the same-UID boundary
# stated below, which no file mode can close. Any other owner is a different
# principal who can rewrite these bytes whenever they like.
#
# THE FALSE POSITIVE THIS BUYS, NAMED RATHER THAN DISCOVERED. A tool owned by
# a SERVICE ACCOUNT -- a Linux runner's toolchain under a build user, a
# linuxbrew prefix, a vendored toolchain installed as its own daemon user --
# is refused here even though nobody is attacking. That is a real cost and it
# falls on honest configurations, which is the failure mode that gets a
# security control deleted rather than fixed, so it is stated in the block the
# reader pastes and the refusal below says what to do about it.
#
# It is not auto-widened, and the reason is that the obvious widening does not
# survive contact: 'trust uids below the system threshold' needs a threshold,
# and there is no single one. macOS numbers its first human user 501 while
# Linux reserves everything under 1000 for system accounts, so any constant
# either trusts a macOS human being or refuses a Linux service account. A
# heuristic that guesses wrong in the first direction is worse than a refusal
# a reader can read and act on.
case "$__wwo" in "$__wwme"|'root') return 1 ;; *) return 0 ;; esac
}
# Every directory from a path up to / — a writable ancestor lets a principal
# rename the directory below it and substitute the whole subtree, so checking
# only the immediate parent leaves the same replacement one level up.
pushgate_world_writable_path() {
__wwp="$1"
while : ; do
pushgate_world_writable "${__wwp:-/}"; __wwr=$?
[ "$__wwr" -eq 1 ] || return "$__wwr"
case "$__wwp" in ""|"/") return 1 ;; esac
__wwp="${__wwp%/*}"
done
}
pushgate_host_stage() {
__stage="$(mktemp -d)" || {
echo 'refusing: could not create a private staging directory' >&2; return 1; }
[ -n "$__stage" ] || {
echo 'refusing: empty staging directory path' >&2; return 1; }
__stage="$(cd -P "$__stage" 2>/dev/null && pwd -P)"
[ -n "$__stage" ] || {
echo 'refusing: could not canonicalize the staging directory, so it cannot be proven outside the workspace' >&2; return 1; }
__stage_pwd="$(cd -P "$PWD" 2>/dev/null && pwd -P)"
[ -n "$__stage_pwd" ] || {
echo 'refusing: cannot resolve the workspace directory, so nothing can be proven outside it' >&2; return 1; }
case "$__stage" in "$__stage_pwd"|"$__stage_pwd"/*)
echo "refusing: TMPDIR stages inside the workspace ($__stage) — verified bytes would sit where the agent this boundary contains can rewrite them" >&2; return 1 ;; esac
# THE WHOLE CHAIN, not the leaf. mktemp -d makes a 0700 directory, and that
# says nothing about the directories above it: a non-sticky world-writable
# ancestor lets another user rename this one away and substitute their own
# between the integrity check and the bash that runs the result. The
# ancestor walk is the same one the resolved tool paths get.
pushgate_world_writable_path "$__stage"; __stage_ww=$?
[ "$__stage_ww" -eq 1 ] || {
echo "refusing: the staging directory, or a directory above it, is world-writable, owned by another principal, or could not be read" >&2; return 1; }
printf '%s\n' "$__stage"
}
pushgate_host_tool() {
__invoke="$(PATH="$PUSHGATE_AMBIENT_PATH"; command -v "$1" 2>/dev/null || true)"
case "$__invoke" in /*) ;; *)
echo "refusing: no host-owned $1 on PATH" >&2; return 1 ;; esac
__tool="$__invoke"
__hops=0
while [ -L "$__tool" ] && [ "$__hops" -lt 16 ]; do
__link="$(readlink "$__tool")"
case "$__link" in
/*) __tool="$__link" ;;
*) __tool="${__tool%/*}/$__link" ;;
esac
__hops=$((__hops + 1))
done
__dir="${__tool%/*}"
__tool="$(cd -P "${__dir:-/}" 2>/dev/null && pwd -P)/${__tool##*/}"
__dir="${__invoke%/*}"
__invoke="$(cd -P "${__dir:-/}" 2>/dev/null && pwd -P)/${__invoke##*/}"
__invoke_dir="${__invoke%/*}"
__pwd="$(cd -P "$PWD" 2>/dev/null && pwd -P)"
[ -n "$__pwd" ] || {
echo "refusing: cannot resolve the workspace directory, so nothing can be excluded from it" >&2; return 1; }
case "$__tool" in "$__pwd"/*)
echo "refusing: $1 resolves inside the workspace ($__tool) — a repository-controlled PATH entry cannot supply it" >&2; return 1 ;; esac
case "$__invoke" in "$__pwd"/*)
echo "refusing: $1 is supplied from inside the workspace ($__invoke) — a repository-controlled PATH entry cannot supply it" >&2; return 1 ;; esac
[ -x "$__tool" ] || { echo "refusing: $__tool is not executable" >&2; return 1; }
pushgate_world_writable "$__tool"; __ww=$?
[ "$__ww" -eq 1 ] || {
echo "refusing: $__tool is world-writable, owned by a user other than you or root, or its permissions could not be read, so it is not provably host-owned. If it is owned by a service account you trust -- a runner toolchain, a linuxbrew prefix -- that is a stated limitation of this check, not an attack: install or resolve the tool from a path owned by you or by root." >&2; return 1; }
# THE RESOLVED TARGET'S OWN DIRECTORY CHAIN, not just the invocation's.
# A symlink on a tight path pointing into a writable directory was accepted:
# the link and its directory both looked fine while the bytes it named could
# be replaced at will. The file's mode says nothing about that — replacing a
# file needs write on its DIRECTORY, so the directory is the thing to check.
pushgate_world_writable_path "${__tool%/*}"; __ww=$?
[ "$__ww" -eq 1 ] || {
echo "refusing: the directory holding $__tool is world-writable, or its permissions could not be read, so $1 could be replaced there" >&2; return 1; }
pushgate_world_writable_path "${__invoke_dir:-/}"; __ww=$?
[ "$__ww" -eq 1 ] || {
echo "refusing: ${__invoke_dir:-/} is world-writable, or its permissions could not be read, so $1 could be repointed" >&2; return 1; }
printf '%s\n' "$__invoke"
}
if pushgate_host_has brew; then
PUSHGATE_BREW="$(pushgate_host_tool brew)" || exit 1
PUSHGATE_PREFIX="$("$PUSHGATE_BREW" --prefix)"
else PUSHGATE_PREFIX='/opt/homebrew'; fi
case "$PUSHGATE_PREFIX" in
"$PWD"/*) echo 'refusing: the installation prefix resolves inside the workspace' >&2; exit 1 ;;
/*) ;;
*) echo 'refusing: the installation prefix is not an absolute path' >&2; exit 1 ;;
esac
PUSHGATE_CURL="$(pushgate_host_tool curl)" || exit 1
if pushgate_host_has shasum; then
PUSHGATE_SHA256="$(pushgate_host_tool shasum)" || exit 1
PUSHGATE_SHA256="$PUSHGATE_SHA256 -a 256"
else PUSHGATE_SHA256="$(pushgate_host_tool sha256sum)" || exit 1; fi
PUSHGATE_BASH="$(pushgate_host_tool bash)" || exit 1
CILOCK_STAGE="$(pushgate_host_stage)" \
&& "$PUSHGATE_CURL" -fsSL https://cilock.dev/dl/v4.5.0/install.sh -o "$CILOCK_STAGE/install.sh" \
&& printf '%s %s\n' '660e298ca2c4f1fde107f085a705ec14f8ade0b7d9a7afacc1652ab29d522e50' "$CILOCK_STAGE/install.sh" \
| $PUSHGATE_SHA256 -c - \
&& CILOCK_VERSION=v4.5.0 CILOCK_BIN_DIR="$PUSHGATE_PREFIX/bin" \
"$PUSHGATE_BASH" "$CILOCK_STAGE/install.sh" \
&& rm -rf "$CILOCK_STAGE" \
&& "$PUSHGATE_PREFIX/bin/cilock" version \
| { read -r __cilock_version; [ "$__cilock_version" = 'cilock 4.5.0' ]; }
On macOS, copy the block, then run bash <(pbpaste) in your terminal. Pasting the block itself fails in the default macOS shells.
For release-policy and offline verification, use the CI/lock binary verification guide.
ALPS 1 · Authenticated
Mermaid source
flowchart LR
A[Coding agent] -->|invokes| C[CI/lock]
C -->|short-lived repository identity| F[Fulcio]
F -->|mandatory RFC 3161| T[TSA]
T --> E[Signed evidence]
X[No static signing key in agent config] -.-> CSummary
The platform authenticates a bounded workload identity and issues short-lived signing material for the exact evidence operation.
Intended for
Repositories that need independently verifiable signer identity and trusted signing time without a static evidence-signing key in agent configuration.
Requirements
After repository setup, the agent invokes the ordinary CI/lock command with no per-run platform, signer, or timestamp flags.
When the agent invokes CI/lock, the identity is always an agent or workload principal: registered workflow OIDC in CI, or a stable registered agent identity. It is never a human platform session inside the agent boundary.
A human-attributed signature comes only from an explicit, server-observed interactive ceremony the human runs personally, outside the agent boundary. Its output is handed off explicitly. The agent never inherits it.
This section used to publish a runnable cilock run block for an agent. It signed with the credential held by the cilock session already on the host, and that credential authenticates the account the session belongs to. So an agent launched from a signed-in shell inherited that principal, and the evidence could not tell the agent's action from the account holder's. The Pushgate agent-policy contract (docs/architecture/pushgate-agent-policy-contract.md in the Judge source tree) forbids that, so we withdrew the block.
The principal it was waiting for now exists. cilock enroll agent mints a time-bound agent principal with its own SPIFFE ID after a person approves it at AAL2, and cilock run then signs as the agent, never as the person. With that identity, ALPS 1 is Supported on a workstation and through the Pushgate mint. The support matrix lists the environments, and the enrolled-agent guide has the commands and the verification. A person's own cilock login is still not an agent identity: an agent must not sign with it.
Evidence a verifier checks
ALPS 0 facts plus authenticated platform issuance, workload, tenant, repository, purpose, audience and time bindings, and the ephemeral signing event.
Benefits and mitigations
Mitigates long-lived static-key theft, cross-repository identity reuse, unsigned or timestamp-less substitution, replay, and stale-proof ambiguity.
Limits
The agent and CI/lock still share OS/user authority. Keyless issuance does not prove no other key, platform session, or socket existed in the agent environment, or that a permitted CI/lock operation was invoked honestly.
ALPS 2 · Constrained
Mermaid source
flowchart LR
subgraph S[Restricted agent sandbox]
A[Coding agent]
end
N[nono tool broker]
subgraph C[Fresh child-tool sandbox]
W[Pinned CI/lock + attestors]
end
F[Platform identity + Fulcio + TSA]
R[Signed in-toto / DSSE evidence]
A -->|cilock run| N -->|exact binary + narrow grant| W
W -->|keyless sign + timestamp| F --> RSummary
An enforced runtime boundary constrains the agent and a trusted external observer records the exact controls applied to the measured process.
Intended for
Repositories where prompt injection or compromised agent behavior must not directly reach signing authority, privileged sockets, host credentials, or unrestricted egress.
Requirements
ALPS 2 adds enforceable filesystem, environment, process, socket, and network boundaries. The agent sees only the repository, a synthetic home, approved model endpoints, and development tools. It receives no platform session, human key, SSH_AUTH_SOCK, signer cache, cloud credential, Keychain proxy, Docker socket, generic signer socket, or direct platform credential.
What the SHA-256 beside each block does and does not prove
Each setup block below writes its configuration file with a here-document, then checks what landed on disk against the digest printed in the same block.
- What it proves
- A paste check. It catches a copy that was truncated, wrapped, or edited on the way to your terminal, which would otherwise install a half-written boundary silently.
- What it does not prove
- It is not provenance and not proof of authenticity. The digest is printed on the same page as the content it covers, so anyone able to change the content here could change the digest here too, and the check would still pass.
The CI/lock installer above is pinned the same way, by a SHA-256 from the release manifest. That is integrity, not a signature. Nothing on this page checks who built it. To check that, use the release-policy guide with a CI/lock you already trust.
Read the profile before you run it. That is why it is printed here rather than downloaded.
The nono, Sandbox Runtime and container examples are containment-only boundary fixtures
All three prove credential and signer containment. None of them is authenticated proof.
None supplies the cumulative ALPS 1 authentication path. The boundary passes only generic environment variables and a synthetic home, and excludes every platform session, so cilock run inside these fixtures cannot produce the authenticated ALPS 1 proof. That holds even where the nono child policy grants narrow platform egress.
Authenticated signing needs an identity issued outside the agent boundary:
- registered workflow OIDC in CI;
- an agent identity a human enrolls on the host;
- an explicit human signing ceremony on the host.
The deployable Sandbox Runtime profile below carries the second, and it is the one example here that runs the Pushgate setup. A deployable ALPS 2 composition also needs an independently mediated CI/lock path bound to such an identity.
Three gaps to check before you trust the boundary
What closes each of these differs, and the callout says which. Sandbox Runtime closes the first and nono cannot. Nothing on this page closes the second, because it needs a different privilege domain. The third is left open on purpose, and you can close it yourself.
A boxed agent that writes .git/hooks/pre-commit, or points core.hooksPath somewhere it controls via .git/config, has not escaped its sandbox. It has arranged for your next Git command to run its code outside the sandbox, with your signing keys and platform session. The boundary that matters is not where the agent runs. It is what runs after.
Sandbox Runtime closes this. Its profile denies writes to .git/hooks/** and .git/config while leaving them readable, so Git keeps working and the hook cannot be planted. That was measured against the runtime, not inferred from the schema.
nono 0.74.0 does not, and cannot. We measured all four ways it could have:
denyblocks reads as well as writes, and Git needs to read.git/configfor every operation, so the boundary stops working at all.readis an additive grant. It does not restrict.read_fileis the same.denycombined with a read re-grant leaves the read blocked.
There is no write-only denial for a subpath of a read-write workdir. The signed receipt for this boundary records that honestly, as hostGitMetadata: allowed and result: open, rather than reporting a pass.
Use the Sandbox Runtime boundary where this matters, or run the agent under a separate UID or in a VM.
Every block below pins PATH to the base system and resolves each third-party tool to an absolute path. It refuses any tool supplied from inside your workspace, and any whose file or parent directory is world-writable.
That defeats a hostile repository: a repository-controlled PATH entry, and a binary another local user could have replaced.
It does not prove the tool is genuine. Anyone already running as your own user can write $HOME/bin/nono or $HOME/bin/gh with ordinary 0755 permissions. That is outside the workspace and not world-writable, so it passes every check on this page:
- a fake
gh attestation verifyexits0; - a fake
nonoreports the expected version; - the agent is recorded as sandboxed while running with no sandbox at all.
The same applies to cosign, curl and CI/lock, and to the attestation check that verifies nono. No file-permission test can close it, because the check runs with exactly the privileges of the process it would have to catch.
The mitigation is a different privilege domain, not a stronger probe. Run the agent under a separate UID, in a container, or in a VM, so that writing $HOME/bin is not something it can do.
The probe tests the other-write bit and not the group-write bit. Homebrew's default prefix is group-writable. Measured on an Apple-silicon Mac, /opt/homebrew/bin is drwxrwxr-x <user>:admin; Intel's /usr/local/bin has the same shape.
So any member of that group can replace nono, gh, cosign or CI/lock before confinement starts, and these checks will still call the result host-owned. Unlike the same-UID case above, that group is a genuinely different principal from you.
We do not reject it automatically. Doing so would refuse every Homebrew-installed tool, which is the install path this page itself recommends, so the choice is yours rather than ours.
If other people have admin on the machine, close it:
chmod g-w /opt/homebrew/bin
Installing these tools somewhere that is not group-writable works too. The probe reports on the resolved binary and every directory above it, so a prefix you have tightened is checked all the way up.
nono 0.74.0
Store this profile outside the repository. command_policies resolves cilock to the exact non-workspace executable, denies direct-exec bypass, and creates a new child-tool sandbox for every approved run or verify invocation. Fake binaries, PATH shadows, and writable replacements receive no CI/lock command policy.
{
"$schema": "https://nono.sh/schemas/nono-profile.schema.json",
"extends": "default",
"meta": {
"name": "pushgate-agent-macos",
"version": "0.74.0",
"description": "Workspace-only coding-agent boundary with a pinned CI/lock child-tool policy"
},
"security": {
"signal_mode": "isolated",
"process_info_mode": "isolated",
"ipc_mode": "shared_memory_only",
"capability_elevation": false
},
"workdir": {
"access": "readwrite"
},
"filesystem": {
"deny": [
"$HOME/.ssh",
"$HOME/.gnupg",
"$HOME/.config/cilock",
"$HOME/.config/gitsign",
"$HOME/.config/sigstore",
"$HOME/.aws",
"$HOME/.azure",
"$HOME/.config/gcloud",
"$HOME/.kube",
"$HOME/.docker",
"$HOME/Library/Application Support/cilock",
"$HOME/Library/Application Support/gitsign",
"$HOME/Library/Application Support/sigstore",
"$HOME/Library/Caches/sigstore",
"$HOME/Library/Keychains",
"$HOME/Library/Containers/com.docker.docker",
"/var/run/docker.sock",
"/private/var/run/docker.sock",
"$HOME/.colima",
"$HOME/.orbstack",
"$HOME/.rd"
]
},
"network": {
"allow_domain": [
"api.openai.com",
"auth.openai.com",
"chatgpt.com",
"api.anthropic.com",
"claude.ai",
"claude.com",
"platform.claude.com",
"api2.cursor.sh",
"cursor.com"
]
},
"environment": {
"allow_vars": [
"PATH",
"TERM",
"COLORTERM",
"LANG",
"LC_ALL",
"TMPDIR"
],
"deny_vars": [
"SSH_AUTH_SOCK",
"GPG_AGENT_INFO",
"DOCKER_HOST",
"CILOCK_*",
"GITSIGN_*",
"SIGSTORE_*",
"AWS_*",
"AZURE_*",
"GOOGLE_*",
"GCP_*",
"KUBECONFIG",
"GH_TOKEN",
"GITHUB_TOKEN"
],
"set_vars": {
"HOME": "$WORKDIR/.pushgate/agent-home",
"XDG_CONFIG_HOME": "$WORKDIR/.pushgate/agent-home/.config",
"XDG_CACHE_HOME": "$WORKDIR/.pushgate/agent-home/.cache",
"CODEX_HOME": "$WORKDIR/.pushgate/agent-home/.codex",
"CLAUDE_CONFIG_DIR": "$WORKDIR/.pushgate/agent-home/.claude",
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1",
"DISABLE_TELEMETRY": "1",
"DISABLE_ERROR_REPORTING": "1"
}
},
"command_policies": {
"deny_direct_exec_bypass": [
"/opt/homebrew/bin/cilock"
],
"commands": {
"cilock": {
"executable": "/opt/homebrew/bin/cilock",
"from": {
"session": {
"sandbox": {
"fs_read": [
"."
],
"fs_write": [
"."
],
"network": {
"allow_domain": [
"platform.testifysec.com"
]
},
"environment": {
"allow_vars": [
"HOME",
"PATH",
"TERM",
"TMPDIR"
]
}
},
"invocation_policy": {
"default": "deny",
"allow": [
{
"argv": {
"prefix": [
"run"
]
}
},
{
"argv": {
"prefix": [
"verify"
]
}
},
{
"argv": {
"exact": [
"version"
]
}
}
]
}
}
}
}
}
}
}
Set up and launch
The block below writes that profile itself, from the same bytes shown above, so nothing here depends on a file you do not have. The SHA-256 check that follows it confirms the paste arrived intact; it is not a provenance or authenticity claim.
# ALPS 0.1 supported environment for this boundary: Apple-silicon macOS.
# The nono profile below pins /opt/homebrew paths and the digest printed
# beside it covers those exact bytes, so the profile cannot be re-pointed per
# host without giving up the paste check. On any other Homebrew prefix this
# block REFUSES rather than installing a boundary that cannot work. Intel macOS
# support is pending.
# ALPS 0.1 TRUSTED BOOTSTRAP — the first thing this block does, before any command runs.
# PATH is pinned to the base system, so every utility below (env, find, mkdir, mktemp, ...)
# is the host's own and never this repository's. The PATH you had is kept aside for two
# uses only: LOCATING the third-party tools this block then verifies, and handing a
# verified tool the PATH its own helpers expect. A second block pasted into the same
# shell keeps the PATH the first one saved, rather than saving the pin as if it were yours.
PUSHGATE_AMBIENT_PATH="${PUSHGATE_AMBIENT_PATH:-$PATH}"
export PATH='/usr/bin:/bin'
# SAME-UID LIMIT — what the checks below do, and what they cannot do.
# pushgate_host_tool refuses a tool supplied from inside this workspace, and refuses one whose
# file or parent directory is world-writable. That catches a repository-controlled PATH entry and
# a binary any OTHER local user could have replaced. It does NOT establish that the tool is
# genuine. Someone already running as YOUR user can write $HOME/bin/nono or $HOME/bin/gh with
# ordinary 0755 permissions: outside the workspace, not world-writable, so it passes every check
# here and is then invoked as the "verified" tool. A fake 'gh attestation verify' exits 0 and a
# fake 'nono' prints the expected version, and the agent is recorded as sandboxed and launched
# with no sandbox at all. This applies to every tool resolved below, cosign, curl and cilock
# included, and to the attestation check that verifies nono.
# No file-permission test can close this: the check runs with exactly the privileges of the
# process it would have to catch. The mitigation is a different privilege domain, not a stronger
# probe — run the agent under a SEPARATE UID, in a container, or in a VM, so that writing
# $HOME/bin is not something it can do in the first place.
#
# GROUP-WRITE, separately, IS yours to close. These checks test the other-write bit and not the
# group-write bit, and Homebrew's default prefix is group-writable: measured on Apple silicon,
# /opt/homebrew/bin is 'drwxrwxr-x <user>:admin' (Intel /usr/local/bin is the same). So any other
# member of that group can replace nono, gh, cosign or cilock before confinement starts and these
# checks still report host-owned. That is a different principal from you, unlike the case above.
# It is not rejected automatically because rejecting it refuses every Homebrew-installed tool,
# which is the install path this guide recommends. If anyone else has admin on this machine,
# close it yourself: chmod g-w /opt/homebrew/bin, or install these tools outside a group-writable
# prefix.
#
# OWNERSHIP ASSUMPTION, and its cost. A resolved tool is accepted only when YOU or ROOT own it,
# because any other owner can rewrite those bytes whenever they like. The cost lands on honest
# setups: a tool owned by a SERVICE ACCOUNT -- a CI runner toolchain under a build user, a
# linuxbrew prefix, a vendored toolchain installed as its own daemon user -- is refused here even
# though nobody is attacking. If that is your machine, it is a stated limit of this check rather than
# a finding: install or resolve the tool from a path owned by you or by root. It is deliberately
# not widened by guessing at a system-uid threshold, because macOS numbers its first human user
# 501 and Linux reserves under 1000 for daemons, so any single threshold would either trust a
# human peer on macOS or refuse a service account on Linux.
pushgate_host_has() { (PATH="$PUSHGATE_AMBIENT_PATH"; command -v "$1") >/dev/null 2>&1; }
pushgate_world_writable() {
__wwl="$(ls -ld "$1" 2>/dev/null)" || return 2
set -- $__wwl
[ $# -ge 3 ] || return 2
__wwo="$3"
__wwl="$1"
case "$__wwl" in ??????????*) ;; *) return 2 ;; esac
# WHO THIS PROCESS IS, resolved BEFORE any branch may answer "trusted".
# Every acceptance below is a statement about the owner, so an acceptance
# reached without this value is an acceptance made without asking the
# question. Recomputed on every call rather than cached across the walk:
# caching would let a __wwme already set in the pasted shell's environment
# decide who this process is, which hands the same-UID attacker a fresh lever
# to buy back a few forks the walk would never have noticed.
__wwme="$(id -un 2>/dev/null)" || return 2
[ -n "$__wwme" ] || return 2
# A STICKY directory is world-writable but restricted-deletion: anyone may
# create their own entries, and only the owner of an existing entry may
# unlink or rename it. That is precisely the question here — can someone
# else swap these bytes — so /tmp and friends are NOT replaceable. Without
# this, the ancestor walk below rejects every tool under /tmp on Linux,
# where TMPDIR is /tmp (drwxrwxrwt); macOS hides the bug because TMPDIR is a
# private per-user /var/folders path. CI caught exactly that difference.
#
# RESTRICTED-DELETION DOES NOT BIND THE DIRECTORY'S OWN OWNER. Sticky stops a
# STRANGER from unlinking somebody else's entry; the owner of the directory
# may still rename or remove anything inside it. So the sticky bit answers
# "can a stranger swap this" and says nothing about the owner, and a 1777
# directory belonging to another principal can replace the verified staging
# subtree between the integrity check and the bash that runs it. That is
# this file's own defect one branch further in — a mode bit accepted as an
# answer to a question about ownership — so the sticky branch asks the same
# question every other acceptance asks instead of returning early.
case "$__wwl" in d????????[tT]*)
case "$__wwo" in "$__wwme"|'root') return 1 ;; *) return 0 ;; esac ;;
esac
__wwl="${__wwl#????????}"
case "$__wwl" in w*) return 0 ;; -*) ;; *) return 2 ;; esac
# WHO OWNS IT, not just who else can write it. A 0755 file has its other-write
# bit clear and is still rewritable at will BY ITS OWNER, so a tool owned by
# another local user was being accepted as host-owned on the strength of a bit
# that says nothing about them. Same defect as checking a directory without its
# ancestors: the thing was validated and its context was not.
#
# Only this user and root qualify. Root because the base system is not
# installed by the reader; this user because that is the same-UID boundary
# stated below, which no file mode can close. Any other owner is a different
# principal who can rewrite these bytes whenever they like.
#
# THE FALSE POSITIVE THIS BUYS, NAMED RATHER THAN DISCOVERED. A tool owned by
# a SERVICE ACCOUNT -- a Linux runner's toolchain under a build user, a
# linuxbrew prefix, a vendored toolchain installed as its own daemon user --
# is refused here even though nobody is attacking. That is a real cost and it
# falls on honest configurations, which is the failure mode that gets a
# security control deleted rather than fixed, so it is stated in the block the
# reader pastes and the refusal below says what to do about it.
#
# It is not auto-widened, and the reason is that the obvious widening does not
# survive contact: 'trust uids below the system threshold' needs a threshold,
# and there is no single one. macOS numbers its first human user 501 while
# Linux reserves everything under 1000 for system accounts, so any constant
# either trusts a macOS human being or refuses a Linux service account. A
# heuristic that guesses wrong in the first direction is worse than a refusal
# a reader can read and act on.
case "$__wwo" in "$__wwme"|'root') return 1 ;; *) return 0 ;; esac
}
# Every directory from a path up to / — a writable ancestor lets a principal
# rename the directory below it and substitute the whole subtree, so checking
# only the immediate parent leaves the same replacement one level up.
pushgate_world_writable_path() {
__wwp="$1"
while : ; do
pushgate_world_writable "${__wwp:-/}"; __wwr=$?
[ "$__wwr" -eq 1 ] || return "$__wwr"
case "$__wwp" in ""|"/") return 1 ;; esac
__wwp="${__wwp%/*}"
done
}
pushgate_host_stage() {
__stage="$(mktemp -d)" || {
echo 'refusing: could not create a private staging directory' >&2; return 1; }
[ -n "$__stage" ] || {
echo 'refusing: empty staging directory path' >&2; return 1; }
__stage="$(cd -P "$__stage" 2>/dev/null && pwd -P)"
[ -n "$__stage" ] || {
echo 'refusing: could not canonicalize the staging directory, so it cannot be proven outside the workspace' >&2; return 1; }
__stage_pwd="$(cd -P "$PWD" 2>/dev/null && pwd -P)"
[ -n "$__stage_pwd" ] || {
echo 'refusing: cannot resolve the workspace directory, so nothing can be proven outside it' >&2; return 1; }
case "$__stage" in "$__stage_pwd"|"$__stage_pwd"/*)
echo "refusing: TMPDIR stages inside the workspace ($__stage) — verified bytes would sit where the agent this boundary contains can rewrite them" >&2; return 1 ;; esac
# THE WHOLE CHAIN, not the leaf. mktemp -d makes a 0700 directory, and that
# says nothing about the directories above it: a non-sticky world-writable
# ancestor lets another user rename this one away and substitute their own
# between the integrity check and the bash that runs the result. The
# ancestor walk is the same one the resolved tool paths get.
pushgate_world_writable_path "$__stage"; __stage_ww=$?
[ "$__stage_ww" -eq 1 ] || {
echo "refusing: the staging directory, or a directory above it, is world-writable, owned by another principal, or could not be read" >&2; return 1; }
printf '%s\n' "$__stage"
}
pushgate_host_tool() {
__invoke="$(PATH="$PUSHGATE_AMBIENT_PATH"; command -v "$1" 2>/dev/null || true)"
case "$__invoke" in /*) ;; *)
echo "refusing: no host-owned $1 on PATH" >&2; return 1 ;; esac
__tool="$__invoke"
__hops=0
while [ -L "$__tool" ] && [ "$__hops" -lt 16 ]; do
__link="$(readlink "$__tool")"
case "$__link" in
/*) __tool="$__link" ;;
*) __tool="${__tool%/*}/$__link" ;;
esac
__hops=$((__hops + 1))
done
__dir="${__tool%/*}"
__tool="$(cd -P "${__dir:-/}" 2>/dev/null && pwd -P)/${__tool##*/}"
__dir="${__invoke%/*}"
__invoke="$(cd -P "${__dir:-/}" 2>/dev/null && pwd -P)/${__invoke##*/}"
__invoke_dir="${__invoke%/*}"
__pwd="$(cd -P "$PWD" 2>/dev/null && pwd -P)"
[ -n "$__pwd" ] || {
echo "refusing: cannot resolve the workspace directory, so nothing can be excluded from it" >&2; return 1; }
case "$__tool" in "$__pwd"/*)
echo "refusing: $1 resolves inside the workspace ($__tool) — a repository-controlled PATH entry cannot supply it" >&2; return 1 ;; esac
case "$__invoke" in "$__pwd"/*)
echo "refusing: $1 is supplied from inside the workspace ($__invoke) — a repository-controlled PATH entry cannot supply it" >&2; return 1 ;; esac
[ -x "$__tool" ] || { echo "refusing: $__tool is not executable" >&2; return 1; }
pushgate_world_writable "$__tool"; __ww=$?
[ "$__ww" -eq 1 ] || {
echo "refusing: $__tool is world-writable, owned by a user other than you or root, or its permissions could not be read, so it is not provably host-owned. If it is owned by a service account you trust -- a runner toolchain, a linuxbrew prefix -- that is a stated limitation of this check, not an attack: install or resolve the tool from a path owned by you or by root." >&2; return 1; }
# THE RESOLVED TARGET'S OWN DIRECTORY CHAIN, not just the invocation's.
# A symlink on a tight path pointing into a writable directory was accepted:
# the link and its directory both looked fine while the bytes it named could
# be replaced at will. The file's mode says nothing about that — replacing a
# file needs write on its DIRECTORY, so the directory is the thing to check.
pushgate_world_writable_path "${__tool%/*}"; __ww=$?
[ "$__ww" -eq 1 ] || {
echo "refusing: the directory holding $__tool is world-writable, or its permissions could not be read, so $1 could be replaced there" >&2; return 1; }
pushgate_world_writable_path "${__invoke_dir:-/}"; __ww=$?
[ "$__ww" -eq 1 ] || {
echo "refusing: ${__invoke_dir:-/} is world-writable, or its permissions could not be read, so $1 could be repointed" >&2; return 1; }
printf '%s\n' "$__invoke"
}
if pushgate_host_has brew; then
PUSHGATE_BREW="$(pushgate_host_tool brew)" || exit 1
PUSHGATE_PREFIX="$("$PUSHGATE_BREW" --prefix)"
else PUSHGATE_PREFIX='/opt/homebrew'; fi
case "$PUSHGATE_PREFIX" in
"$PWD"/*) echo 'refusing: the installation prefix resolves inside the workspace' >&2; exit 1 ;;
/*) ;;
*) echo 'refusing: the installation prefix is not an absolute path' >&2; exit 1 ;;
esac
[ "$PUSHGATE_PREFIX" = '/opt/homebrew' ] || {
echo "refusing: this profile pins /opt/homebrew/bin/cilock and its digest, and this host's Homebrew prefix is $PUSHGATE_PREFIX — the pinned nono profile is Apple-silicon only" >&2; exit 1; }
if pushgate_host_has shasum; then
PUSHGATE_SHA256="$(pushgate_host_tool shasum)" || exit 1
PUSHGATE_SHA256="$PUSHGATE_SHA256 -a 256"
else PUSHGATE_SHA256="$(pushgate_host_tool sha256sum)" || exit 1; fi
PUSHGATE_NONO="$(pushgate_host_tool nono)" || exit 1
PUSHGATE_GH="$(pushgate_host_tool gh)" || exit 1
"$PUSHGATE_GH" attestation verify "$PUSHGATE_NONO" --repo nolabs-ai/nono >/dev/null || {
echo 'refusing: this nono carries no verifiable build provenance from nolabs-ai/nono' >&2; exit 1; }
test "$("$PUSHGATE_NONO" --version)" = "nono 0.74.0" || {
echo 'refusing: nono 0.74.0 is required' >&2; exit 1; }
mkdir -p "$HOME/.config/pushgate" || exit 1
printf '%s\n' "$PUSHGATE_NONO" > "$HOME/.config/pushgate/nono.path" || exit 1
$PUSHGATE_SHA256 "$PUSHGATE_NONO" > "$HOME/.config/pushgate/nono.sha256" || exit 1
if pushgate_host_has shasum; then
PUSHGATE_SHA256="$(pushgate_host_tool shasum)" || exit 1
PUSHGATE_SHA256="$PUSHGATE_SHA256 -a 256"
else PUSHGATE_SHA256="$(pushgate_host_tool sha256sum)" || exit 1; fi
cat > "$HOME/.config/pushgate/nono-macos.json" <<'PUSHGATE_NONO_PROFILE'
{
"$schema": "https://nono.sh/schemas/nono-profile.schema.json",
"extends": "default",
"meta": {
"name": "pushgate-agent-macos",
"version": "0.74.0",
"description": "Workspace-only coding-agent boundary with a pinned CI/lock child-tool policy"
},
"security": {
"signal_mode": "isolated",
"process_info_mode": "isolated",
"ipc_mode": "shared_memory_only",
"capability_elevation": false
},
"workdir": {
"access": "readwrite"
},
"filesystem": {
"deny": [
"$HOME/.ssh",
"$HOME/.gnupg",
"$HOME/.config/cilock",
"$HOME/.config/gitsign",
"$HOME/.config/sigstore",
"$HOME/.aws",
"$HOME/.azure",
"$HOME/.config/gcloud",
"$HOME/.kube",
"$HOME/.docker",
"$HOME/Library/Application Support/cilock",
"$HOME/Library/Application Support/gitsign",
"$HOME/Library/Application Support/sigstore",
"$HOME/Library/Caches/sigstore",
"$HOME/Library/Keychains",
"$HOME/Library/Containers/com.docker.docker",
"/var/run/docker.sock",
"/private/var/run/docker.sock",
"$HOME/.colima",
"$HOME/.orbstack",
"$HOME/.rd"
]
},
"network": {
"allow_domain": [
"api.openai.com",
"auth.openai.com",
"chatgpt.com",
"api.anthropic.com",
"claude.ai",
"claude.com",
"platform.claude.com",
"api2.cursor.sh",
"cursor.com"
]
},
"environment": {
"allow_vars": [
"PATH",
"TERM",
"COLORTERM",
"LANG",
"LC_ALL",
"TMPDIR"
],
"deny_vars": [
"SSH_AUTH_SOCK",
"GPG_AGENT_INFO",
"DOCKER_HOST",
"CILOCK_*",
"GITSIGN_*",
"SIGSTORE_*",
"AWS_*",
"AZURE_*",
"GOOGLE_*",
"GCP_*",
"KUBECONFIG",
"GH_TOKEN",
"GITHUB_TOKEN"
],
"set_vars": {
"HOME": "$WORKDIR/.pushgate/agent-home",
"XDG_CONFIG_HOME": "$WORKDIR/.pushgate/agent-home/.config",
"XDG_CACHE_HOME": "$WORKDIR/.pushgate/agent-home/.cache",
"CODEX_HOME": "$WORKDIR/.pushgate/agent-home/.codex",
"CLAUDE_CONFIG_DIR": "$WORKDIR/.pushgate/agent-home/.claude",
"CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1",
"DISABLE_TELEMETRY": "1",
"DISABLE_ERROR_REPORTING": "1"
}
},
"command_policies": {
"deny_direct_exec_bypass": [
"/opt/homebrew/bin/cilock"
],
"commands": {
"cilock": {
"executable": "/opt/homebrew/bin/cilock",
"from": {
"session": {
"sandbox": {
"fs_read": [
"."
],
"fs_write": [
"."
],
"network": {
"allow_domain": [
"platform.testifysec.com"
]
},
"environment": {
"allow_vars": [
"HOME",
"PATH",
"TERM",
"TMPDIR"
]
}
},
"invocation_policy": {
"default": "deny",
"allow": [
{
"argv": {
"prefix": [
"run"
]
}
},
{
"argv": {
"prefix": [
"verify"
]
}
},
{
"argv": {
"exact": [
"version"
]
}
}
]
}
}
}
}
}
}
}
PUSHGATE_NONO_PROFILE
printf '%s %s\n' 'af1c5d4c64f6e1b83adf5ca0b38761c8575bdc4bf4adf95bd8c27e4475de166e' "$HOME/.config/pushgate/nono-macos.json" | $PUSHGATE_SHA256 -c - \
|| { rm -f "$HOME/.config/pushgate/nono-macos.json"; \
echo 'refusing: the pasted block did not survive the copy — paste it again' >&2; exit 1; }
# A SYMLINK IS NOT A DIRECTORY YOU OWN.
# `mkdir -p` is satisfied by an existing symlink and FOLLOWS it, while the guard below only
# asks whether the literal path .pushgate is ignored and untracked. So an agent that had already
# written .pushgate/agent-home as a link to an unignored directory got the next launch to deposit
# its model-provider credentials there, where `git add -A` commits them. Every level is refused
# as a link first, and the created home is then proven to canonicalize back inside .pushgate.
pushgate_not_a_link() {
[ ! -L "$1" ] || {
echo "refusing: $1 is a symlink — the agent home must be real directories inside .pushgate, or credentials land wherever it points" >&2; return 1; }
}
pushgate_not_a_link .pushgate || exit 1
pushgate_not_a_link .pushgate/agent-home || exit 1
pushgate_not_a_link .pushgate/agent-home/.config || exit 1
pushgate_not_a_link .pushgate/agent-home/.cache || exit 1
pushgate_not_a_link .pushgate/agent-home/.codex || exit 1
pushgate_not_a_link .pushgate/agent-home/.claude || exit 1
mkdir -p .pushgate/agent-home/.config .pushgate/agent-home/.cache \
.pushgate/agent-home/.codex .pushgate/agent-home/.claude
__ah_real="$(cd -P .pushgate/agent-home 2>/dev/null && pwd -P)"
__pg_real="$(cd -P .pushgate 2>/dev/null && pwd -P)"
[ -n "$__ah_real" ] && [ -n "$__pg_real" ] || {
echo 'refusing: could not canonicalize the agent home, so it cannot be proven to sit inside .pushgate' >&2; exit 1; }
case "$__ah_real" in "$__pg_real"/*) ;; *)
echo "refusing: the agent home resolves outside .pushgate ($__ah_real) — credentials would be written where the ignore rule does not reach" >&2; exit 1 ;; esac
PUSHGATE_GIT="$(pushgate_host_tool git)" || exit 1
"$PUSHGATE_GIT" -c core.fsmonitor= -c core.hooksPath=/dev/null -c core.pager=cat check-ignore -q .pushgate || {
echo 'refusing: .pushgate is not git-ignored: add ".pushgate/" to .gitignore, or to .git/info/exclude to keep it out of your diff' >&2; exit 1; }
[ -z "$("$PUSHGATE_GIT" -c core.fsmonitor= -c core.hooksPath=/dev/null -c core.pager=cat ls-files -- .pushgate)" ] || {
echo 'refusing: .pushgate is tracked — run "git rm -r --cached .pushgate"' >&2; exit 1; }
__pg_tw="$(pwd -P)" && [ -n "$__pg_tw" ] || {
echo 'refusing: could not resolve the workspace directory' >&2; exit 1; }
__pg_tm="${TMPDIR:-/tmp}"
__pg_tp="$(cd -P "$__pg_tm" 2>/dev/null && pwd -P)" || __pg_tp="$__pg_tm"
pushgate_agent_writable() {
case "$1/" in "${__pg_tw%/}"/*|"${__pg_tm%/}"/*|"${__pg_tp%/}"/*) return 0 ;; esac
__pg_n="${1#/private}"
case "$1/" in /private/*) ;; *) __pg_n="$1" ;; esac
case "$__pg_n/" in /tmp/*|/var/folders/*|/dev/*) return 0 ;; esac
return 1
}
__pg_nl='
'
__pg_r1="$HOME/.config/pushgate" __pg_r2="$HOME/.local/share/pushgate"
pushgate_no_acl() {
case "$1/" in "${__pg_r1%/}"/*|"${__pg_r2%/}"/*) __pg_am=any ;; *) __pg_am=allow ;; esac
if __pg_al="$(ls -lde "$1" 2>/dev/null)"; then
case "$__pg_al" in *"$__pg_nl"*) __pg_al="${__pg_al#*"$__pg_nl"}" ;; *) return 0 ;; esac
if [ "$__pg_am" = allow ]; then
case "$__pg_al" in *" allow"*) ;; *) return 0 ;; esac
fi
elif __pg_al="$(ls -ld "$1" 2>/dev/null)"; then
case "${__pg_al%% *}" in ??????????+*) ;; *) return 0 ;; esac
fi
echo "refusing: $1 carries an access control list -- or its ACL could not be read -- so another account could be granted write to what this boundary trusts" >&2; exit 1
}
pushgate_trusted_path() {
case "$1" in /*) ;; *) echo "refusing: $1 is not an absolute path" >&2; exit 1 ;; esac
__pg_left="${1#/}" __pg_at= __pg_hops=0
while [ -n "$__pg_left" ]; do
__pg_c="${__pg_left%%/*}"
case "$__pg_left" in */*) __pg_left="${__pg_left#*/}" ;; *) __pg_left= ;; esac
[ -n "$__pg_c" ] && [ "$__pg_c" != . ] || continue
[ "$__pg_c" != .. ] || { __pg_at="${__pg_at%/*}"; continue; }
__pg_at="$__pg_at/$__pg_c"
! pushgate_agent_writable "$__pg_at" || {
echo "refusing: $1 passes through $__pg_at, which is inside the workspace or inside a directory a sandboxed agent can write, so the agent could redirect it" >&2; exit 1; }
if [ -e "$__pg_at" ] || [ -L "$__pg_at" ]; then pushgate_no_acl "$__pg_at"; fi
[ -L "$__pg_at" ] || continue
__pg_hops=$((__pg_hops + 1))
__pg_l="$(readlink "$__pg_at")" && [ -n "$__pg_l" ] && [ "$__pg_hops" -le 32 ] || {
echo "refusing: could not resolve the link $__pg_at in $1" >&2; exit 1; }
case "$__pg_l" in /*) __pg_at= ;; *) __pg_at="${__pg_at%/*}" ;; esac
__pg_left="${__pg_l#/}${__pg_left:+/$__pg_left}"
done
case "$__pg_tw/" in "${__pg_at%/}"/*)
echo "refusing: the workspace $__pg_tw is inside $__pg_at ($1), which this boundary trusts" >&2; exit 1 ;; esac
}
pushgate_real_dirs() {
__pg_left="${1#/}" __pg_at=
while [ -n "$__pg_left" ]; do
__pg_c="${__pg_left%%/*}"
case "$__pg_left" in */*) __pg_left="${__pg_left#*/}" ;; *) __pg_left= ;; esac
[ -n "$__pg_c" ] || continue
__pg_at="$__pg_at/$__pg_c"
# Not there yet is not a link; whatever reads it next refuses on its own.
[ -e "$__pg_at" ] || [ -L "$__pg_at" ] || return 0
[ ! -L "$__pg_at" ] && [ -d "$__pg_at" ] || {
echo "refusing: $__pg_at is a symlink or not a directory, and $1 must sit on real directories no sandbox can redirect -- run the setup block above again" >&2; exit 1; }
pushgate_no_acl "$__pg_at"
case "$__pg_at/" in "${HOME%/}"/*) [ -O "$__pg_at" ] || {
echo "refusing: $__pg_at is not owned by you, so another account could rewrite $1" >&2; exit 1; } ;; esac
done
}
pushgate_real_file() {
__pg_f="$1"
pushgate_real_dirs "${__pg_f%/*}"
[ -e "$1" ] || [ -L "$1" ] || return 0
[ -f "$1" ] && [ ! -L "$1" ] || {
echo "refusing: $1 is a symlink or not a regular file -- run the setup block above again" >&2; exit 1; }
pushgate_no_acl "$1"
}
pushgate_trusted_path "$HOME/.config/pushgate"
pushgate_trusted_path "$HOME/.local/share/pushgate"
pushgate_real_dirs "$HOME/.config/pushgate"
pushgate_trusted_path "$HOME/.config/pushgate/nono.path"
pushgate_trusted_path "$HOME/.config/pushgate/nono.sha256"
pushgate_trusted_path "$HOME/.config/pushgate/nono-macos.json"
pushgate_real_file "$HOME/.config/pushgate/nono.path"
pushgate_real_file "$HOME/.config/pushgate/nono.sha256"
pushgate_real_file "$HOME/.config/pushgate/nono-macos.json"
PUSHGATE_NONO= PUSHGATE_NONO_DIGEST=
read -r PUSHGATE_NONO 2>/dev/null < "$HOME/.config/pushgate/nono.path" || true
[ -n "$PUSHGATE_NONO" ] || {
echo 'refusing: no attested nono on record — run the setup block above first' >&2; exit 1; }
pushgate_trusted_path "$PUSHGATE_NONO"
read -r PUSHGATE_NONO_DIGEST PUSHGATE_NONO_DIGEST_SUBJECT 2>/dev/null < "$HOME/.config/pushgate/nono.sha256" || true
[ -n "$PUSHGATE_NONO_DIGEST" ] || {
echo 'refusing: no recorded digest for the attested nono — run the setup block above first' >&2; exit 1; }
PUSHGATE_RUN="$(umask 077 && mktemp -d "$HOME/.config/pushgate/launch.XXXXXX")" && [ -n "$PUSHGATE_RUN" ] || {
echo 'refusing: could not create a private launch directory' >&2; exit 1; }
trap 'rm -rf "$PUSHGATE_RUN"' EXIT
pushgate_trusted_path "$PUSHGATE_RUN"
pushgate_real_dirs "$PUSHGATE_RUN"
cp "$PUSHGATE_NONO" "$PUSHGATE_RUN/nono" || {
echo 'refusing: could not make a private copy of nono' >&2; exit 1; }
pushgate_no_acl "$PUSHGATE_RUN/nono"
printf '%s %s\n' "$PUSHGATE_NONO_DIGEST" "$PUSHGATE_RUN/nono" | $PUSHGATE_SHA256 -c - >/dev/null 2>&1 || {
echo 'refusing: the attested nono has changed since it was verified' >&2; exit 1; }
cp "$HOME/.config/pushgate/nono-macos.json" "$PUSHGATE_RUN/nono-macos.json" || {
echo 'refusing: could not make a private copy of the nono profile' >&2; exit 1; }
pushgate_no_acl "$PUSHGATE_RUN/nono-macos.json"
printf '%s %s\n' 'af1c5d4c64f6e1b83adf5ca0b38761c8575bdc4bf4adf95bd8c27e4475de166e' "$PUSHGATE_RUN/nono-macos.json" | $PUSHGATE_SHA256 -c - >/dev/null 2>&1 || {
echo 'refusing: the installed nono profile is not the one this page publishes -- run the setup block above again' >&2; exit 1; }
PATH="$PUSHGATE_AMBIENT_PATH" "$PUSHGATE_RUN/nono" run --profile "$PUSHGATE_RUN/nono-macos.json" \
--workdir "$PWD" --allow-cwd -- codex --sandbox danger-full-access --ask-for-approval on-request
On macOS, copy the block, then run bash <(pbpaste) in your terminal. Pasting the block itself fails in the default macOS shells.
macOS with Anthropic Sandbox Runtime 0.0.73
Sandbox Runtime 0.0.73
This containment-only fixture keeps runtime settings outside the workspace. A broad read deny for user and temporary volumes is followed by narrow workspace and toolchain carve-outs. Unix sockets, local listeners, Apple Events, and the weaker nested modes remain disabled. Because it provides neither platform signing egress nor a separate CI/lock mediator, it does not by itself satisfy cumulative ALPS 1 or ALPS 2.
allowPty is on. It is Sandbox Runtime's pseudo-terminal grant, and the interactive agent this block launches needs it: without it, Claude Code starts but takes no keyboard input, and Codex exits at launch.
{
"network": {
"allowedDomains": [
"api.openai.com",
"auth.openai.com",
"chatgpt.com",
"api.anthropic.com",
"claude.ai",
"claude.com",
"platform.claude.com",
"api2.cursor.sh",
"cursor.com"
],
"deniedDomains": [],
"allowUnixSockets": [],
"allowAllUnixSockets": false,
"allowLocalBinding": false
},
"filesystem": {
"denyRead": [
"/Users",
"/private",
"/Volumes"
],
"allowRead": [
".",
"./**",
"/bin",
"/dev",
"/Library",
"/opt/homebrew",
"/private/var/select",
"/System",
"/usr",
"/usr/local"
],
"allowWrite": [
".",
"./**"
],
"denyWrite": [
"./.git/hooks/**",
"./.git/config"
]
},
"enableWeakerNestedSandbox": false,
"enableWeakerNetworkIsolation": false,
"allowAppleEvents": false,
"allowPty": true
}
Set up and launch
As above, the block writes the settings file from the bytes shown here rather than from any path in a repository. The SHA-256 check that follows it confirms the paste arrived intact; it is not a provenance or authenticity claim.
What the launch checks, and what it relies on. Before it starts the agent, the launch refuses if any directory on the way to the settings file or to the runtime under ~/.local/share/pushgate is a symbolic link, is not owned by you, or lies inside the workspace or a directory a sandboxed agent can write. It then hands Sandbox Runtime a private copy of the settings file, checked against the digest above, and removes that copy when it exits. It also refuses if the interpreter the runtime's launcher names lies in the workspace or a directory a sandboxed agent can write. The runtime itself is an npm package with no published digest, so the launch holds its installed tree to a closed world instead: it refuses unless every entry is a regular file, directory or symbolic link of yours that no other account can write and that carries no access control list, no file is hard-linked to a name outside the tree, and every symbolic link is relative and never leaves the tree, even for one step. The bytes of the files inside it are not yet checked against a digest. The nono launch runs private, digest-verified copies of its binary and profile instead.
# ALPS 0.1 supported environment for this boundary: macOS on Apple silicon and
# on Intel. Linux is covered by the container boundary, not by this block.
# ALPS 0.1 TRUSTED BOOTSTRAP — the first thing this block does, before any command runs.
# PATH is pinned to the base system, so every utility below (env, find, mkdir, mktemp, ...)
# is the host's own and never this repository's. The PATH you had is kept aside for two
# uses only: LOCATING the third-party tools this block then verifies, and handing a
# verified tool the PATH its own helpers expect. A second block pasted into the same
# shell keeps the PATH the first one saved, rather than saving the pin as if it were yours.
PUSHGATE_AMBIENT_PATH="${PUSHGATE_AMBIENT_PATH:-$PATH}"
export PATH='/usr/bin:/bin'
# SAME-UID LIMIT — what the checks below do, and what they cannot do.
# pushgate_host_tool refuses a tool supplied from inside this workspace, and refuses one whose
# file or parent directory is world-writable. That catches a repository-controlled PATH entry and
# a binary any OTHER local user could have replaced. It does NOT establish that the tool is
# genuine. Someone already running as YOUR user can write $HOME/bin/nono or $HOME/bin/gh with
# ordinary 0755 permissions: outside the workspace, not world-writable, so it passes every check
# here and is then invoked as the "verified" tool. A fake 'gh attestation verify' exits 0 and a
# fake 'nono' prints the expected version, and the agent is recorded as sandboxed and launched
# with no sandbox at all. This applies to every tool resolved below, cosign, curl and cilock
# included, and to the attestation check that verifies nono.
# No file-permission test can close this: the check runs with exactly the privileges of the
# process it would have to catch. The mitigation is a different privilege domain, not a stronger
# probe — run the agent under a SEPARATE UID, in a container, or in a VM, so that writing
# $HOME/bin is not something it can do in the first place.
#
# GROUP-WRITE, separately, IS yours to close. These checks test the other-write bit and not the
# group-write bit, and Homebrew's default prefix is group-writable: measured on Apple silicon,
# /opt/homebrew/bin is 'drwxrwxr-x <user>:admin' (Intel /usr/local/bin is the same). So any other
# member of that group can replace nono, gh, cosign or cilock before confinement starts and these
# checks still report host-owned. That is a different principal from you, unlike the case above.
# It is not rejected automatically because rejecting it refuses every Homebrew-installed tool,
# which is the install path this guide recommends. If anyone else has admin on this machine,
# close it yourself: chmod g-w /opt/homebrew/bin, or install these tools outside a group-writable
# prefix.
#
# OWNERSHIP ASSUMPTION, and its cost. A resolved tool is accepted only when YOU or ROOT own it,
# because any other owner can rewrite those bytes whenever they like. The cost lands on honest
# setups: a tool owned by a SERVICE ACCOUNT -- a CI runner toolchain under a build user, a
# linuxbrew prefix, a vendored toolchain installed as its own daemon user -- is refused here even
# though nobody is attacking. If that is your machine, it is a stated limit of this check rather than
# a finding: install or resolve the tool from a path owned by you or by root. It is deliberately
# not widened by guessing at a system-uid threshold, because macOS numbers its first human user
# 501 and Linux reserves under 1000 for daemons, so any single threshold would either trust a
# human peer on macOS or refuse a service account on Linux.
pushgate_host_has() { (PATH="$PUSHGATE_AMBIENT_PATH"; command -v "$1") >/dev/null 2>&1; }
pushgate_world_writable() {
__wwl="$(ls -ld "$1" 2>/dev/null)" || return 2
set -- $__wwl
[ $# -ge 3 ] || return 2
__wwo="$3"
__wwl="$1"
case "$__wwl" in ??????????*) ;; *) return 2 ;; esac
# WHO THIS PROCESS IS, resolved BEFORE any branch may answer "trusted".
# Every acceptance below is a statement about the owner, so an acceptance
# reached without this value is an acceptance made without asking the
# question. Recomputed on every call rather than cached across the walk:
# caching would let a __wwme already set in the pasted shell's environment
# decide who this process is, which hands the same-UID attacker a fresh lever
# to buy back a few forks the walk would never have noticed.
__wwme="$(id -un 2>/dev/null)" || return 2
[ -n "$__wwme" ] || return 2
# A STICKY directory is world-writable but restricted-deletion: anyone may
# create their own entries, and only the owner of an existing entry may
# unlink or rename it. That is precisely the question here — can someone
# else swap these bytes — so /tmp and friends are NOT replaceable. Without
# this, the ancestor walk below rejects every tool under /tmp on Linux,
# where TMPDIR is /tmp (drwxrwxrwt); macOS hides the bug because TMPDIR is a
# private per-user /var/folders path. CI caught exactly that difference.
#
# RESTRICTED-DELETION DOES NOT BIND THE DIRECTORY'S OWN OWNER. Sticky stops a
# STRANGER from unlinking somebody else's entry; the owner of the directory
# may still rename or remove anything inside it. So the sticky bit answers
# "can a stranger swap this" and says nothing about the owner, and a 1777
# directory belonging to another principal can replace the verified staging
# subtree between the integrity check and the bash that runs it. That is
# this file's own defect one branch further in — a mode bit accepted as an
# answer to a question about ownership — so the sticky branch asks the same
# question every other acceptance asks instead of returning early.
case "$__wwl" in d????????[tT]*)
case "$__wwo" in "$__wwme"|'root') return 1 ;; *) return 0 ;; esac ;;
esac
__wwl="${__wwl#????????}"
case "$__wwl" in w*) return 0 ;; -*) ;; *) return 2 ;; esac
# WHO OWNS IT, not just who else can write it. A 0755 file has its other-write
# bit clear and is still rewritable at will BY ITS OWNER, so a tool owned by
# another local user was being accepted as host-owned on the strength of a bit
# that says nothing about them. Same defect as checking a directory without its
# ancestors: the thing was validated and its context was not.
#
# Only this user and root qualify. Root because the base system is not
# installed by the reader; this user because that is the same-UID boundary
# stated below, which no file mode can close. Any other owner is a different
# principal who can rewrite these bytes whenever they like.
#
# THE FALSE POSITIVE THIS BUYS, NAMED RATHER THAN DISCOVERED. A tool owned by
# a SERVICE ACCOUNT -- a Linux runner's toolchain under a build user, a
# linuxbrew prefix, a vendored toolchain installed as its own daemon user --
# is refused here even though nobody is attacking. That is a real cost and it
# falls on honest configurations, which is the failure mode that gets a
# security control deleted rather than fixed, so it is stated in the block the
# reader pastes and the refusal below says what to do about it.
#
# It is not auto-widened, and the reason is that the obvious widening does not
# survive contact: 'trust uids below the system threshold' needs a threshold,
# and there is no single one. macOS numbers its first human user 501 while
# Linux reserves everything under 1000 for system accounts, so any constant
# either trusts a macOS human being or refuses a Linux service account. A
# heuristic that guesses wrong in the first direction is worse than a refusal
# a reader can read and act on.
case "$__wwo" in "$__wwme"|'root') return 1 ;; *) return 0 ;; esac
}
# Every directory from a path up to / — a writable ancestor lets a principal
# rename the directory below it and substitute the whole subtree, so checking
# only the immediate parent leaves the same replacement one level up.
pushgate_world_writable_path() {
__wwp="$1"
while : ; do
pushgate_world_writable "${__wwp:-/}"; __wwr=$?
[ "$__wwr" -eq 1 ] || return "$__wwr"
case "$__wwp" in ""|"/") return 1 ;; esac
__wwp="${__wwp%/*}"
done
}
pushgate_host_stage() {
__stage="$(mktemp -d)" || {
echo 'refusing: could not create a private staging directory' >&2; return 1; }
[ -n "$__stage" ] || {
echo 'refusing: empty staging directory path' >&2; return 1; }
__stage="$(cd -P "$__stage" 2>/dev/null && pwd -P)"
[ -n "$__stage" ] || {
echo 'refusing: could not canonicalize the staging directory, so it cannot be proven outside the workspace' >&2; return 1; }
__stage_pwd="$(cd -P "$PWD" 2>/dev/null && pwd -P)"
[ -n "$__stage_pwd" ] || {
echo 'refusing: cannot resolve the workspace directory, so nothing can be proven outside it' >&2; return 1; }
case "$__stage" in "$__stage_pwd"|"$__stage_pwd"/*)
echo "refusing: TMPDIR stages inside the workspace ($__stage) — verified bytes would sit where the agent this boundary contains can rewrite them" >&2; return 1 ;; esac
# THE WHOLE CHAIN, not the leaf. mktemp -d makes a 0700 directory, and that
# says nothing about the directories above it: a non-sticky world-writable
# ancestor lets another user rename this one away and substitute their own
# between the integrity check and the bash that runs the result. The
# ancestor walk is the same one the resolved tool paths get.
pushgate_world_writable_path "$__stage"; __stage_ww=$?
[ "$__stage_ww" -eq 1 ] || {
echo "refusing: the staging directory, or a directory above it, is world-writable, owned by another principal, or could not be read" >&2; return 1; }
printf '%s\n' "$__stage"
}
pushgate_host_tool() {
__invoke="$(PATH="$PUSHGATE_AMBIENT_PATH"; command -v "$1" 2>/dev/null || true)"
case "$__invoke" in /*) ;; *)
echo "refusing: no host-owned $1 on PATH" >&2; return 1 ;; esac
__tool="$__invoke"
__hops=0
while [ -L "$__tool" ] && [ "$__hops" -lt 16 ]; do
__link="$(readlink "$__tool")"
case "$__link" in
/*) __tool="$__link" ;;
*) __tool="${__tool%/*}/$__link" ;;
esac
__hops=$((__hops + 1))
done
__dir="${__tool%/*}"
__tool="$(cd -P "${__dir:-/}" 2>/dev/null && pwd -P)/${__tool##*/}"
__dir="${__invoke%/*}"
__invoke="$(cd -P "${__dir:-/}" 2>/dev/null && pwd -P)/${__invoke##*/}"
__invoke_dir="${__invoke%/*}"
__pwd="$(cd -P "$PWD" 2>/dev/null && pwd -P)"
[ -n "$__pwd" ] || {
echo "refusing: cannot resolve the workspace directory, so nothing can be excluded from it" >&2; return 1; }
case "$__tool" in "$__pwd"/*)
echo "refusing: $1 resolves inside the workspace ($__tool) — a repository-controlled PATH entry cannot supply it" >&2; return 1 ;; esac
case "$__invoke" in "$__pwd"/*)
echo "refusing: $1 is supplied from inside the workspace ($__invoke) — a repository-controlled PATH entry cannot supply it" >&2; return 1 ;; esac
[ -x "$__tool" ] || { echo "refusing: $__tool is not executable" >&2; return 1; }
pushgate_world_writable "$__tool"; __ww=$?
[ "$__ww" -eq 1 ] || {
echo "refusing: $__tool is world-writable, owned by a user other than you or root, or its permissions could not be read, so it is not provably host-owned. If it is owned by a service account you trust -- a runner toolchain, a linuxbrew prefix -- that is a stated limitation of this check, not an attack: install or resolve the tool from a path owned by you or by root." >&2; return 1; }
# THE RESOLVED TARGET'S OWN DIRECTORY CHAIN, not just the invocation's.
# A symlink on a tight path pointing into a writable directory was accepted:
# the link and its directory both looked fine while the bytes it named could
# be replaced at will. The file's mode says nothing about that — replacing a
# file needs write on its DIRECTORY, so the directory is the thing to check.
pushgate_world_writable_path "${__tool%/*}"; __ww=$?
[ "$__ww" -eq 1 ] || {
echo "refusing: the directory holding $__tool is world-writable, or its permissions could not be read, so $1 could be replaced there" >&2; return 1; }
pushgate_world_writable_path "${__invoke_dir:-/}"; __ww=$?
[ "$__ww" -eq 1 ] || {
echo "refusing: ${__invoke_dir:-/} is world-writable, or its permissions could not be read, so $1 could be repointed" >&2; return 1; }
printf '%s\n' "$__invoke"
}
PUSHGATE_NPM="$(pushgate_host_tool npm)" || exit 1
PATH="/opt/homebrew/opt/node@22/bin:/opt/homebrew/bin:/usr/local/opt/node@22/bin:/usr/local/bin:/usr/bin:/bin" "$PUSHGATE_NPM" install --prefix "$HOME/.local/share/pushgate/srt-0.0.73" \
--save-exact @anthropic-ai/[email protected] || {
echo 'refusing: the pinned Sandbox Runtime install failed' >&2; exit 1; }
test -x "$HOME/.local/share/pushgate/srt-0.0.73/node_modules/.bin/srt" || {
echo 'refusing: no Sandbox Runtime at $HOME/.local/share/pushgate/srt-0.0.73' >&2; exit 1; }
mkdir -p "$HOME/.config/pushgate" || exit 1
if pushgate_host_has shasum; then
PUSHGATE_SHA256="$(pushgate_host_tool shasum)" || exit 1
PUSHGATE_SHA256="$PUSHGATE_SHA256 -a 256"
else PUSHGATE_SHA256="$(pushgate_host_tool sha256sum)" || exit 1; fi
cat > "$HOME/.config/pushgate/srt-macos.json" <<'PUSHGATE_SRT_SETTINGS'
{
"network": {
"allowedDomains": [
"api.openai.com",
"auth.openai.com",
"chatgpt.com",
"api.anthropic.com",
"claude.ai",
"claude.com",
"platform.claude.com",
"api2.cursor.sh",
"cursor.com"
],
"deniedDomains": [],
"allowUnixSockets": [],
"allowAllUnixSockets": false,
"allowLocalBinding": false
},
"filesystem": {
"denyRead": [
"/Users",
"/private",
"/Volumes"
],
"allowRead": [
".",
"./**",
"/bin",
"/dev",
"/Library",
"/opt/homebrew",
"/private/var/select",
"/System",
"/usr",
"/usr/local"
],
"allowWrite": [
".",
"./**"
],
"denyWrite": [
"./.git/hooks/**",
"./.git/config"
]
},
"enableWeakerNestedSandbox": false,
"enableWeakerNetworkIsolation": false,
"allowAppleEvents": false,
"allowPty": true
}
PUSHGATE_SRT_SETTINGS
printf '%s %s\n' 'a3ba8b7beae510d7e0b01e3c8f31354e374bea173cb8981c5ba8df329b9f710f' "$HOME/.config/pushgate/srt-macos.json" | $PUSHGATE_SHA256 -c - \
|| { rm -f "$HOME/.config/pushgate/srt-macos.json"; \
echo 'refusing: the pasted block did not survive the copy — paste it again' >&2; exit 1; }
# A SYMLINK IS NOT A DIRECTORY YOU OWN.
# `mkdir -p` is satisfied by an existing symlink and FOLLOWS it, while the guard below only
# asks whether the literal path .pushgate is ignored and untracked. So an agent that had already
# written .pushgate/agent-home as a link to an unignored directory got the next launch to deposit
# its model-provider credentials there, where `git add -A` commits them. Every level is refused
# as a link first, and the created home is then proven to canonicalize back inside .pushgate.
pushgate_not_a_link() {
[ ! -L "$1" ] || {
echo "refusing: $1 is a symlink — the agent home must be real directories inside .pushgate, or credentials land wherever it points" >&2; return 1; }
}
pushgate_not_a_link .pushgate || exit 1
pushgate_not_a_link .pushgate/agent-home || exit 1
pushgate_not_a_link .pushgate/agent-home/.config || exit 1
pushgate_not_a_link .pushgate/agent-home/.cache || exit 1
pushgate_not_a_link .pushgate/agent-home/.codex || exit 1
pushgate_not_a_link .pushgate/agent-home/.claude || exit 1
mkdir -p .pushgate/agent-home/.config .pushgate/agent-home/.cache \
.pushgate/agent-home/.codex .pushgate/agent-home/.claude
__ah_real="$(cd -P .pushgate/agent-home 2>/dev/null && pwd -P)"
__pg_real="$(cd -P .pushgate 2>/dev/null && pwd -P)"
[ -n "$__ah_real" ] && [ -n "$__pg_real" ] || {
echo 'refusing: could not canonicalize the agent home, so it cannot be proven to sit inside .pushgate' >&2; exit 1; }
case "$__ah_real" in "$__pg_real"/*) ;; *)
echo "refusing: the agent home resolves outside .pushgate ($__ah_real) — credentials would be written where the ignore rule does not reach" >&2; exit 1 ;; esac
PUSHGATE_GIT="$(pushgate_host_tool git)" || exit 1
"$PUSHGATE_GIT" -c core.fsmonitor= -c core.hooksPath=/dev/null -c core.pager=cat check-ignore -q .pushgate || {
echo 'refusing: .pushgate is not git-ignored: add ".pushgate/" to .gitignore, or to .git/info/exclude to keep it out of your diff' >&2; exit 1; }
[ -z "$("$PUSHGATE_GIT" -c core.fsmonitor= -c core.hooksPath=/dev/null -c core.pager=cat ls-files -- .pushgate)" ] || {
echo 'refusing: .pushgate is tracked — run "git rm -r --cached .pushgate"' >&2; exit 1; }
__pg_tw="$(pwd -P)" && [ -n "$__pg_tw" ] || {
echo 'refusing: could not resolve the workspace directory' >&2; exit 1; }
__pg_tm="${TMPDIR:-/tmp}"
__pg_tp="$(cd -P "$__pg_tm" 2>/dev/null && pwd -P)" || __pg_tp="$__pg_tm"
pushgate_agent_writable() {
case "$1/" in "${__pg_tw%/}"/*|"${__pg_tm%/}"/*|"${__pg_tp%/}"/*) return 0 ;; esac
__pg_n="${1#/private}"
case "$1/" in /private/*) ;; *) __pg_n="$1" ;; esac
case "$__pg_n/" in /tmp/*|/var/folders/*|/dev/*) return 0 ;; esac
return 1
}
__pg_nl='
'
__pg_r1="$HOME/.config/pushgate" __pg_r2="$HOME/.local/share/pushgate"
pushgate_no_acl() {
case "$1/" in "${__pg_r1%/}"/*|"${__pg_r2%/}"/*) __pg_am=any ;; *) __pg_am=allow ;; esac
if __pg_al="$(ls -lde "$1" 2>/dev/null)"; then
case "$__pg_al" in *"$__pg_nl"*) __pg_al="${__pg_al#*"$__pg_nl"}" ;; *) return 0 ;; esac
if [ "$__pg_am" = allow ]; then
case "$__pg_al" in *" allow"*) ;; *) return 0 ;; esac
fi
elif __pg_al="$(ls -ld "$1" 2>/dev/null)"; then
case "${__pg_al%% *}" in ??????????+*) ;; *) return 0 ;; esac
fi
echo "refusing: $1 carries an access control list -- or its ACL could not be read -- so another account could be granted write to what this boundary trusts" >&2; exit 1
}
pushgate_trusted_path() {
case "$1" in /*) ;; *) echo "refusing: $1 is not an absolute path" >&2; exit 1 ;; esac
__pg_left="${1#/}" __pg_at= __pg_hops=0
while [ -n "$__pg_left" ]; do
__pg_c="${__pg_left%%/*}"
case "$__pg_left" in */*) __pg_left="${__pg_left#*/}" ;; *) __pg_left= ;; esac
[ -n "$__pg_c" ] && [ "$__pg_c" != . ] || continue
[ "$__pg_c" != .. ] || { __pg_at="${__pg_at%/*}"; continue; }
__pg_at="$__pg_at/$__pg_c"
! pushgate_agent_writable "$__pg_at" || {
echo "refusing: $1 passes through $__pg_at, which is inside the workspace or inside a directory a sandboxed agent can write, so the agent could redirect it" >&2; exit 1; }
if [ -e "$__pg_at" ] || [ -L "$__pg_at" ]; then pushgate_no_acl "$__pg_at"; fi
[ -L "$__pg_at" ] || continue
__pg_hops=$((__pg_hops + 1))
__pg_l="$(readlink "$__pg_at")" && [ -n "$__pg_l" ] && [ "$__pg_hops" -le 32 ] || {
echo "refusing: could not resolve the link $__pg_at in $1" >&2; exit 1; }
case "$__pg_l" in /*) __pg_at= ;; *) __pg_at="${__pg_at%/*}" ;; esac
__pg_left="${__pg_l#/}${__pg_left:+/$__pg_left}"
done
case "$__pg_tw/" in "${__pg_at%/}"/*)
echo "refusing: the workspace $__pg_tw is inside $__pg_at ($1), which this boundary trusts" >&2; exit 1 ;; esac
}
pushgate_real_dirs() {
__pg_left="${1#/}" __pg_at=
while [ -n "$__pg_left" ]; do
__pg_c="${__pg_left%%/*}"
case "$__pg_left" in */*) __pg_left="${__pg_left#*/}" ;; *) __pg_left= ;; esac
[ -n "$__pg_c" ] || continue
__pg_at="$__pg_at/$__pg_c"
# Not there yet is not a link; whatever reads it next refuses on its own.
[ -e "$__pg_at" ] || [ -L "$__pg_at" ] || return 0
[ ! -L "$__pg_at" ] && [ -d "$__pg_at" ] || {
echo "refusing: $__pg_at is a symlink or not a directory, and $1 must sit on real directories no sandbox can redirect -- run the setup block above again" >&2; exit 1; }
pushgate_no_acl "$__pg_at"
case "$__pg_at/" in "${HOME%/}"/*) [ -O "$__pg_at" ] || {
echo "refusing: $__pg_at is not owned by you, so another account could rewrite $1" >&2; exit 1; } ;; esac
done
}
pushgate_real_file() {
__pg_f="$1"
pushgate_real_dirs "${__pg_f%/*}"
[ -e "$1" ] || [ -L "$1" ] || return 0
[ -f "$1" ] && [ ! -L "$1" ] || {
echo "refusing: $1 is a symlink or not a regular file -- run the setup block above again" >&2; exit 1; }
pushgate_no_acl "$1"
}
pushgate_trusted_path "$HOME/.config/pushgate"
pushgate_trusted_path "$HOME/.local/share/pushgate"
pushgate_real_dirs "$HOME/.config/pushgate"
pushgate_trusted_path "$HOME/.config/pushgate/srt-macos.json"
pushgate_trusted_path "$HOME/.local/share/pushgate/srt-0.0.73"
pushgate_trusted_path "$HOME/.local/share/pushgate/srt-0.0.73/node_modules/.bin/srt"
pushgate_real_dirs "$HOME/.local/share/pushgate/srt-0.0.73"
__pg_sb=
read -r __pg_sb < "$HOME/.local/share/pushgate/srt-0.0.73/node_modules/.bin/srt" || true
case "$__pg_sb" in '#!'*)
set -- ${__pg_sb#??}
__pg_ip="$1" __pg_ia="$2"
pushgate_trusted_path "$__pg_ip"
if [ "${__pg_ip##*/}" = env ]; then
__pg_ip="$(PATH="/opt/homebrew/opt/node@22/bin:/opt/homebrew/bin:/usr/local/opt/node@22/bin:/usr/local/bin:/usr/bin:/bin" command -v "$__pg_ia")" && [ -n "$__pg_ip" ] || {
echo "refusing: the Sandbox Runtime interpreter $__pg_ia is not on the launch PATH" >&2; exit 1; }
pushgate_trusted_path "$__pg_ip"
fi ;;
esac
__pg_sr="$(cd -P "$HOME/.local/share/pushgate/srt-0.0.73" 2>/dev/null && pwd -P)" && [ -n "$__pg_sr" ] || {
echo 'refusing: no Sandbox Runtime at $HOME/.local/share/pushgate/srt-0.0.73 -- run the setup block above first' >&2; exit 1; }
__pg_bad="$(find "$__pg_sr" ! -type f ! -type d ! -type l -o ! -user "$(id -u)" -o -type f -links +1 \
-o ! -type l -perm -0020 -o ! -type l -perm -0002 -o -name "*$__pg_nl*")" && [ -z "$__pg_bad" ] || {
echo "refusing: the Sandbox Runtime tree may hold only your own regular files, directories and links, with no hard links and nothing another account can write -- these are not: $__pg_bad" >&2; exit 1; }
if ls -lde "$__pg_sr" >/dev/null 2>&1; then
__pg_ls="$(ls -lRAe "$__pg_sr")" && __pg_re='^ *[0-9][0-9]*: '
else
__pg_ls="$(ls -lRA "$__pg_sr")" && __pg_re='^[^ ][^ ][^ ][^ ][^ ][^ ][^ ][^ ][^ ][^ ][+]'
fi || {
echo 'refusing: could not list the Sandbox Runtime tree to read its access control lists' >&2; exit 1; }
printf '%s\n' "$__pg_ls" | grep -Eq "$__pg_re"
[ $? -eq 1 ] || {
echo "refusing: an entry in the Sandbox Runtime tree carries an access control list -- or its ACLs could not be read -- so another account could be granted write to it" >&2; exit 1; }
pushgate_tree_link() {
__pg_left="$1" __pg_at= __pg_hops=0
while [ -n "$__pg_left" ]; do
__pg_c="${__pg_left%%/*}"
case "$__pg_left" in */*) __pg_left="${__pg_left#*/}" ;; *) __pg_left= ;; esac
[ -n "$__pg_c" ] && [ "$__pg_c" != . ] || continue
if [ "$__pg_c" = .. ]; then
[ -n "$__pg_at" ] || {
echo "refusing: the link $__pg_sr/$1 climbs out of the Sandbox Runtime tree, where another sandbox could rewrite what it reaches" >&2; exit 1; }
__pg_at="${__pg_at%/*}"; continue
fi
__pg_at="$__pg_at/$__pg_c"
[ -L "$__pg_sr$__pg_at" ] || continue
__pg_hops=$((__pg_hops + 1))
__pg_l="$(readlink "$__pg_sr$__pg_at")" && [ -n "$__pg_l" ] && [ "$__pg_hops" -le 40 ] || {
echo "refusing: could not resolve the link $__pg_sr/$1 inside the Sandbox Runtime tree -- it loops or takes more than 40 hops" >&2; exit 1; }
case "$__pg_l" in /*)
echo "refusing: the link $__pg_sr$__pg_at in the Sandbox Runtime tree is absolute ($__pg_l) -- every link in it must be relative and stay inside the tree" >&2; exit 1 ;; esac
__pg_at="${__pg_at%/*}"
__pg_left="$__pg_l${__pg_left:+/$__pg_left}"
done
}
__pg_links="$(find "$__pg_sr" -type l)" || {
echo 'refusing: could not list the links in the Sandbox Runtime tree' >&2; exit 1; }
printf '%s\n' "$__pg_links" | while IFS= read -r __pg_ln; do
[ -z "$__pg_ln" ] || pushgate_tree_link "${__pg_ln#"$__pg_sr"/}"
done || exit 1
pushgate_real_file "$HOME/.config/pushgate/srt-macos.json"
PUSHGATE_RUN="$(umask 077 && mktemp -d "$HOME/.config/pushgate/launch.XXXXXX")" && [ -n "$PUSHGATE_RUN" ] || {
echo 'refusing: could not create a private launch directory' >&2; exit 1; }
trap 'rm -rf "$PUSHGATE_RUN"' EXIT
pushgate_trusted_path "$PUSHGATE_RUN"
pushgate_real_dirs "$PUSHGATE_RUN"
cp "$HOME/.config/pushgate/srt-macos.json" "$PUSHGATE_RUN/srt-macos.json" || {
echo 'refusing: could not make a private copy of the Sandbox Runtime settings' >&2; exit 1; }
pushgate_no_acl "$PUSHGATE_RUN/srt-macos.json"
printf '%s %s\n' 'a3ba8b7beae510d7e0b01e3c8f31354e374bea173cb8981c5ba8df329b9f710f' "$PUSHGATE_RUN/srt-macos.json" | $PUSHGATE_SHA256 -c - >/dev/null 2>&1 || {
echo 'refusing: the installed Sandbox Runtime settings is not the one this page publishes -- run the setup block above again' >&2; exit 1; }
env -i HOME="$PWD/.pushgate/agent-home" \
XDG_CONFIG_HOME="$PWD/.pushgate/agent-home/.config" \
CODEX_HOME="$PWD/.pushgate/agent-home/.codex" \
CLAUDE_CONFIG_DIR="$PWD/.pushgate/agent-home/.claude" \
PATH="/opt/homebrew/opt/node@22/bin:/opt/homebrew/bin:/usr/local/opt/node@22/bin:/usr/local/bin:/usr/bin:/bin" \
TERM="${TERM:-xterm-256color}" \
"$HOME/.local/share/pushgate/srt-0.0.73/node_modules/.bin/srt" \
--settings "$PUSHGATE_RUN/srt-macos.json" -- codex --sandbox danger-full-access --ask-for-approval on-request
On macOS, copy the block, then run bash <(pbpaste) in your terminal. Pasting the block itself fails in the default macOS shells.
macOS: a Sandbox Runtime profile that runs the Pushgate setup
Deployable Sandbox Runtime 0.0.73
The fixture above contains an agent but cannot run the setup: it resolves no hostname, allows only model endpoints, and denies the CI/lock state.
This profile, srt-macos-deployable.json, is that fixture plus the grants the setup was measured to need, and nothing else. Every denial above is kept, including read-only .git/hooks and .git/config.
It is not ALPS 2. No trusted observer records the boundary, and the trustd grant below means the network allowlist is not a hard egress boundary. With an enrolled agent identity, what the agent produces inside it is ALPS 1 evidence.
enableWeakerNetworkIsolation lets processes inside reach com.apple.trustd.agent. Go, and so CI/lock and govulncheck, verify TLS through it and fail with x509: OSStatus -26276 without it.
trustd runs outside the sandbox and fetches certificate URLs itself: OCSP, CRL and issuer downloads. A process inside can therefore make it contact a host that is not on the allowlist. Treat anything the agent can read as something it could exfiltrate.
If that is not acceptable, do not use this profile. Go tools cannot verify TLS in the containment fixture.
| Grant | Why it is needed |
|---|---|
/private/etc/hosts | Every client inside reaches the filtering proxy at "localhost". Without the hosts file that name does not resolve, so no request leaves at all. |
/private/etc/ssl | curl and git read their OpenSSL configuration and CA bundle here. Without it every HTTPS call fails before it connects. |
/private/var/db/timezone | The system time zone database; /usr/share/zoneinfo links into it. Without it a named zone silently becomes UTC: TZ=America/New_York date prints UTC. |
/private/var/folders/*/*/T/pushgate-agent-* | Read and write. The per-workspace agent state the launch creates outside the checkout: TMPDIR (T), and the CI/lock state and Go caches (C). Only directories named pushgate-agent-*, never the rest of the per-user directory. |
/private/var/folders/*/*/T/pushgate-agent-*/**/* | Read and write. The per-workspace agent state the launch creates outside the checkout: TMPDIR (T), and the CI/lock state and Go caches (C). Only directories named pushgate-agent-*, never the rest of the per-user directory. |
/private/var/folders/*/*/C/pushgate-agent-* | Read and write. The per-workspace agent state the launch creates outside the checkout: TMPDIR (T), and the CI/lock state and Go caches (C). Only directories named pushgate-agent-*, never the rest of the per-user directory. |
/private/var/folders/*/*/C/pushgate-agent-*/**/* | Read and write. The per-workspace agent state the launch creates outside the checkout: TMPDIR (T), and the CI/lock state and Go caches (C). Only directories named pushgate-agent-*, never the rest of the per-user directory. |
enableWeakerNetworkIsolation | Lets processes reach com.apple.trustd.agent. Go, and so CI/lock and govulncheck, verify TLS through trustd on macOS and fail with x509: OSStatus -26276 without it. This is the caveat below. |
pushgate.dev platform.testifysec.com cilock.dev github.com api.github.com raw.githubusercontent.com objects.githubusercontent.com codeload.github.com proxy.golang.org sum.golang.org vuln.go.dev storage.googleapis.com | Each one is needed by a named step: fetching the setup, enrollment status and signing, the CI/lock manifest, Git over HTTPS, GitHub release and archive downloads, the Go module proxy and checksum database, the govulncheck database, and a Go toolchain switch. Every host but two failed its step when it alone was dropped. Two were not probed on their own:
The list covers enrolling, pushing, |
One limit of the glob. The profile is one file for every repository, so the pushgate-agent-* grant covers every workspace's state on this macOS account: an agent in one repository can read the CI/lock credential enrolled for another. Keep repositories that must stay apart under different macOS users.
Loopback stays closed. allowLocalBinding also lets the sandbox connect to every service on the host's 127.0.0.1, a browser debug port included. So these run on the host instead, with the same enrolled identity: cilock enroll agent, whose browser callback binds 127.0.0.1, and any test suite that starts an httptest server.
Smaller consequences of the same profile:
- The whole per-user temp directory is not granted, so Apple's
/usr/bin/gitprintscouldn't create cache file ... xcrun_dbon every call. The command still works; installing Git from Homebrew avoids the message. - macOS
mktempignores TMPDIR, so inside the sandbox usemktemp -p "$TMPDIR". SSL_CERT_FILEis not set, because Go on macOS ignores it.
Set up, enroll on the host, and launch
Run the Sandbox Runtime block above first; this block uses the runtime it installed.
It writes the profile, creates the agent's state outside the checkout in a per-workspace directory under the per-user cache and temp directories, prints that CILOCK_STATE_DIR, and launches Codex. Codex runs with --sandbox danger-full-access because its own sandbox cannot nest inside this one. The SHA-256 check that follows it confirms the paste arrived intact; it is not a provenance or authenticity claim.
# ALPS 0.1 supported environment for this boundary: macOS on Apple silicon and
# on Intel. Linux is covered by the container boundary, not by this block.
# Needs the Sandbox Runtime installed by the block above. Enroll and configure
# Git on the HOST, with the CILOCK_STATE_DIR this block prints, before the agent signs.
# ALPS 0.1 TRUSTED BOOTSTRAP — the first thing this block does, before any command runs.
# PATH is pinned to the base system, so every utility below (env, find, mkdir, mktemp, ...)
# is the host's own and never this repository's. The PATH you had is kept aside for two
# uses only: LOCATING the third-party tools this block then verifies, and handing a
# verified tool the PATH its own helpers expect. A second block pasted into the same
# shell keeps the PATH the first one saved, rather than saving the pin as if it were yours.
PUSHGATE_AMBIENT_PATH="${PUSHGATE_AMBIENT_PATH:-$PATH}"
export PATH='/usr/bin:/bin'
# SAME-UID LIMIT — what the checks below do, and what they cannot do.
# pushgate_host_tool refuses a tool supplied from inside this workspace, and refuses one whose
# file or parent directory is world-writable. That catches a repository-controlled PATH entry and
# a binary any OTHER local user could have replaced. It does NOT establish that the tool is
# genuine. Someone already running as YOUR user can write $HOME/bin/nono or $HOME/bin/gh with
# ordinary 0755 permissions: outside the workspace, not world-writable, so it passes every check
# here and is then invoked as the "verified" tool. A fake 'gh attestation verify' exits 0 and a
# fake 'nono' prints the expected version, and the agent is recorded as sandboxed and launched
# with no sandbox at all. This applies to every tool resolved below, cosign, curl and cilock
# included, and to the attestation check that verifies nono.
# No file-permission test can close this: the check runs with exactly the privileges of the
# process it would have to catch. The mitigation is a different privilege domain, not a stronger
# probe — run the agent under a SEPARATE UID, in a container, or in a VM, so that writing
# $HOME/bin is not something it can do in the first place.
#
# GROUP-WRITE, separately, IS yours to close. These checks test the other-write bit and not the
# group-write bit, and Homebrew's default prefix is group-writable: measured on Apple silicon,
# /opt/homebrew/bin is 'drwxrwxr-x <user>:admin' (Intel /usr/local/bin is the same). So any other
# member of that group can replace nono, gh, cosign or cilock before confinement starts and these
# checks still report host-owned. That is a different principal from you, unlike the case above.
# It is not rejected automatically because rejecting it refuses every Homebrew-installed tool,
# which is the install path this guide recommends. If anyone else has admin on this machine,
# close it yourself: chmod g-w /opt/homebrew/bin, or install these tools outside a group-writable
# prefix.
#
# OWNERSHIP ASSUMPTION, and its cost. A resolved tool is accepted only when YOU or ROOT own it,
# because any other owner can rewrite those bytes whenever they like. The cost lands on honest
# setups: a tool owned by a SERVICE ACCOUNT -- a CI runner toolchain under a build user, a
# linuxbrew prefix, a vendored toolchain installed as its own daemon user -- is refused here even
# though nobody is attacking. If that is your machine, it is a stated limit of this check rather than
# a finding: install or resolve the tool from a path owned by you or by root. It is deliberately
# not widened by guessing at a system-uid threshold, because macOS numbers its first human user
# 501 and Linux reserves under 1000 for daemons, so any single threshold would either trust a
# human peer on macOS or refuse a service account on Linux.
pushgate_host_has() { (PATH="$PUSHGATE_AMBIENT_PATH"; command -v "$1") >/dev/null 2>&1; }
pushgate_world_writable() {
__wwl="$(ls -ld "$1" 2>/dev/null)" || return 2
set -- $__wwl
[ $# -ge 3 ] || return 2
__wwo="$3"
__wwl="$1"
case "$__wwl" in ??????????*) ;; *) return 2 ;; esac
# WHO THIS PROCESS IS, resolved BEFORE any branch may answer "trusted".
# Every acceptance below is a statement about the owner, so an acceptance
# reached without this value is an acceptance made without asking the
# question. Recomputed on every call rather than cached across the walk:
# caching would let a __wwme already set in the pasted shell's environment
# decide who this process is, which hands the same-UID attacker a fresh lever
# to buy back a few forks the walk would never have noticed.
__wwme="$(id -un 2>/dev/null)" || return 2
[ -n "$__wwme" ] || return 2
# A STICKY directory is world-writable but restricted-deletion: anyone may
# create their own entries, and only the owner of an existing entry may
# unlink or rename it. That is precisely the question here — can someone
# else swap these bytes — so /tmp and friends are NOT replaceable. Without
# this, the ancestor walk below rejects every tool under /tmp on Linux,
# where TMPDIR is /tmp (drwxrwxrwt); macOS hides the bug because TMPDIR is a
# private per-user /var/folders path. CI caught exactly that difference.
#
# RESTRICTED-DELETION DOES NOT BIND THE DIRECTORY'S OWN OWNER. Sticky stops a
# STRANGER from unlinking somebody else's entry; the owner of the directory
# may still rename or remove anything inside it. So the sticky bit answers
# "can a stranger swap this" and says nothing about the owner, and a 1777
# directory belonging to another principal can replace the verified staging
# subtree between the integrity check and the bash that runs it. That is
# this file's own defect one branch further in — a mode bit accepted as an
# answer to a question about ownership — so the sticky branch asks the same
# question every other acceptance asks instead of returning early.
case "$__wwl" in d????????[tT]*)
case "$__wwo" in "$__wwme"|'root') return 1 ;; *) return 0 ;; esac ;;
esac
__wwl="${__wwl#????????}"
case "$__wwl" in w*) return 0 ;; -*) ;; *) return 2 ;; esac
# WHO OWNS IT, not just who else can write it. A 0755 file has its other-write
# bit clear and is still rewritable at will BY ITS OWNER, so a tool owned by
# another local user was being accepted as host-owned on the strength of a bit
# that says nothing about them. Same defect as checking a directory without its
# ancestors: the thing was validated and its context was not.
#
# Only this user and root qualify. Root because the base system is not
# installed by the reader; this user because that is the same-UID boundary
# stated below, which no file mode can close. Any other owner is a different
# principal who can rewrite these bytes whenever they like.
#
# THE FALSE POSITIVE THIS BUYS, NAMED RATHER THAN DISCOVERED. A tool owned by
# a SERVICE ACCOUNT -- a Linux runner's toolchain under a build user, a
# linuxbrew prefix, a vendored toolchain installed as its own daemon user --
# is refused here even though nobody is attacking. That is a real cost and it
# falls on honest configurations, which is the failure mode that gets a
# security control deleted rather than fixed, so it is stated in the block the
# reader pastes and the refusal below says what to do about it.
#
# It is not auto-widened, and the reason is that the obvious widening does not
# survive contact: 'trust uids below the system threshold' needs a threshold,
# and there is no single one. macOS numbers its first human user 501 while
# Linux reserves everything under 1000 for system accounts, so any constant
# either trusts a macOS human being or refuses a Linux service account. A
# heuristic that guesses wrong in the first direction is worse than a refusal
# a reader can read and act on.
case "$__wwo" in "$__wwme"|'root') return 1 ;; *) return 0 ;; esac
}
# Every directory from a path up to / — a writable ancestor lets a principal
# rename the directory below it and substitute the whole subtree, so checking
# only the immediate parent leaves the same replacement one level up.
pushgate_world_writable_path() {
__wwp="$1"
while : ; do
pushgate_world_writable "${__wwp:-/}"; __wwr=$?
[ "$__wwr" -eq 1 ] || return "$__wwr"
case "$__wwp" in ""|"/") return 1 ;; esac
__wwp="${__wwp%/*}"
done
}
pushgate_host_stage() {
__stage="$(mktemp -d)" || {
echo 'refusing: could not create a private staging directory' >&2; return 1; }
[ -n "$__stage" ] || {
echo 'refusing: empty staging directory path' >&2; return 1; }
__stage="$(cd -P "$__stage" 2>/dev/null && pwd -P)"
[ -n "$__stage" ] || {
echo 'refusing: could not canonicalize the staging directory, so it cannot be proven outside the workspace' >&2; return 1; }
__stage_pwd="$(cd -P "$PWD" 2>/dev/null && pwd -P)"
[ -n "$__stage_pwd" ] || {
echo 'refusing: cannot resolve the workspace directory, so nothing can be proven outside it' >&2; return 1; }
case "$__stage" in "$__stage_pwd"|"$__stage_pwd"/*)
echo "refusing: TMPDIR stages inside the workspace ($__stage) — verified bytes would sit where the agent this boundary contains can rewrite them" >&2; return 1 ;; esac
# THE WHOLE CHAIN, not the leaf. mktemp -d makes a 0700 directory, and that
# says nothing about the directories above it: a non-sticky world-writable
# ancestor lets another user rename this one away and substitute their own
# between the integrity check and the bash that runs the result. The
# ancestor walk is the same one the resolved tool paths get.
pushgate_world_writable_path "$__stage"; __stage_ww=$?
[ "$__stage_ww" -eq 1 ] || {
echo "refusing: the staging directory, or a directory above it, is world-writable, owned by another principal, or could not be read" >&2; return 1; }
printf '%s\n' "$__stage"
}
pushgate_host_tool() {
__invoke="$(PATH="$PUSHGATE_AMBIENT_PATH"; command -v "$1" 2>/dev/null || true)"
case "$__invoke" in /*) ;; *)
echo "refusing: no host-owned $1 on PATH" >&2; return 1 ;; esac
__tool="$__invoke"
__hops=0
while [ -L "$__tool" ] && [ "$__hops" -lt 16 ]; do
__link="$(readlink "$__tool")"
case "$__link" in
/*) __tool="$__link" ;;
*) __tool="${__tool%/*}/$__link" ;;
esac
__hops=$((__hops + 1))
done
__dir="${__tool%/*}"
__tool="$(cd -P "${__dir:-/}" 2>/dev/null && pwd -P)/${__tool##*/}"
__dir="${__invoke%/*}"
__invoke="$(cd -P "${__dir:-/}" 2>/dev/null && pwd -P)/${__invoke##*/}"
__invoke_dir="${__invoke%/*}"
__pwd="$(cd -P "$PWD" 2>/dev/null && pwd -P)"
[ -n "$__pwd" ] || {
echo "refusing: cannot resolve the workspace directory, so nothing can be excluded from it" >&2; return 1; }
case "$__tool" in "$__pwd"/*)
echo "refusing: $1 resolves inside the workspace ($__tool) — a repository-controlled PATH entry cannot supply it" >&2; return 1 ;; esac
case "$__invoke" in "$__pwd"/*)
echo "refusing: $1 is supplied from inside the workspace ($__invoke) — a repository-controlled PATH entry cannot supply it" >&2; return 1 ;; esac
[ -x "$__tool" ] || { echo "refusing: $__tool is not executable" >&2; return 1; }
pushgate_world_writable "$__tool"; __ww=$?
[ "$__ww" -eq 1 ] || {
echo "refusing: $__tool is world-writable, owned by a user other than you or root, or its permissions could not be read, so it is not provably host-owned. If it is owned by a service account you trust -- a runner toolchain, a linuxbrew prefix -- that is a stated limitation of this check, not an attack: install or resolve the tool from a path owned by you or by root." >&2; return 1; }
# THE RESOLVED TARGET'S OWN DIRECTORY CHAIN, not just the invocation's.
# A symlink on a tight path pointing into a writable directory was accepted:
# the link and its directory both looked fine while the bytes it named could
# be replaced at will. The file's mode says nothing about that — replacing a
# file needs write on its DIRECTORY, so the directory is the thing to check.
pushgate_world_writable_path "${__tool%/*}"; __ww=$?
[ "$__ww" -eq 1 ] || {
echo "refusing: the directory holding $__tool is world-writable, or its permissions could not be read, so $1 could be replaced there" >&2; return 1; }
pushgate_world_writable_path "${__invoke_dir:-/}"; __ww=$?
[ "$__ww" -eq 1 ] || {
echo "refusing: ${__invoke_dir:-/} is world-writable, or its permissions could not be read, so $1 could be repointed" >&2; return 1; }
printf '%s\n' "$__invoke"
}
if pushgate_host_has shasum; then
PUSHGATE_SHA256="$(pushgate_host_tool shasum)" || exit 1
PUSHGATE_SHA256="$PUSHGATE_SHA256 -a 256"
else PUSHGATE_SHA256="$(pushgate_host_tool sha256sum)" || exit 1; fi
test -x "$HOME/.local/share/pushgate/srt-0.0.73/node_modules/.bin/srt" || {
echo 'refusing: no Sandbox Runtime at $HOME/.local/share/pushgate/srt-0.0.73, run the Sandbox Runtime block above first' >&2; exit 1; }
mkdir -p "$HOME/.config/pushgate" || exit 1
if pushgate_host_has shasum; then
PUSHGATE_SHA256="$(pushgate_host_tool shasum)" || exit 1
PUSHGATE_SHA256="$PUSHGATE_SHA256 -a 256"
else PUSHGATE_SHA256="$(pushgate_host_tool sha256sum)" || exit 1; fi
cat > "$HOME/.config/pushgate/srt-macos-deployable.json" <<'PUSHGATE_SRT_DEPLOYABLE_SETTINGS'
{
"network": {
"allowedDomains": [
"api.openai.com",
"auth.openai.com",
"chatgpt.com",
"api.anthropic.com",
"claude.ai",
"claude.com",
"platform.claude.com",
"api2.cursor.sh",
"cursor.com",
"pushgate.dev",
"platform.testifysec.com",
"cilock.dev",
"github.com",
"api.github.com",
"raw.githubusercontent.com",
"objects.githubusercontent.com",
"codeload.github.com",
"proxy.golang.org",
"sum.golang.org",
"vuln.go.dev",
"storage.googleapis.com"
],
"deniedDomains": [],
"allowUnixSockets": [],
"allowAllUnixSockets": false,
"allowLocalBinding": false
},
"filesystem": {
"denyRead": [
"/Users",
"/private",
"/Volumes"
],
"allowRead": [
".",
"./**",
"/bin",
"/dev",
"/Library",
"/opt/homebrew",
"/private/var/select",
"/System",
"/usr",
"/usr/local",
"/private/etc/hosts",
"/private/etc/ssl",
"/private/var/db/timezone",
"/private/var/folders/*/*/T/pushgate-agent-*",
"/private/var/folders/*/*/T/pushgate-agent-*/**/*",
"/private/var/folders/*/*/C/pushgate-agent-*",
"/private/var/folders/*/*/C/pushgate-agent-*/**/*"
],
"allowWrite": [
".",
"./**",
"/private/var/folders/*/*/T/pushgate-agent-*",
"/private/var/folders/*/*/T/pushgate-agent-*/**/*",
"/private/var/folders/*/*/C/pushgate-agent-*",
"/private/var/folders/*/*/C/pushgate-agent-*/**/*"
],
"denyWrite": [
"./.git/hooks/**",
"./.git/config"
]
},
"enableWeakerNestedSandbox": false,
"enableWeakerNetworkIsolation": true,
"allowAppleEvents": false,
"allowPty": true
}
PUSHGATE_SRT_DEPLOYABLE_SETTINGS
printf '%s %s\n' '280886bfdd9e3f1eded02c6b544454e337a4bd59e5d2e944771efdbaf597b1af' "$HOME/.config/pushgate/srt-macos-deployable.json" | $PUSHGATE_SHA256 -c - \
|| { rm -f "$HOME/.config/pushgate/srt-macos-deployable.json"; \
echo 'refusing: the pasted block did not survive the copy — paste it again' >&2; exit 1; }
# A SYMLINK IS NOT A DIRECTORY YOU OWN.
# `mkdir -p` is satisfied by an existing symlink and FOLLOWS it, while the guard below only
# asks whether the literal path .pushgate is ignored and untracked. So an agent that had already
# written .pushgate/agent-home as a link to an unignored directory got the next launch to deposit
# its model-provider credentials there, where `git add -A` commits them. Every level is refused
# as a link first, and the created home is then proven to canonicalize back inside .pushgate.
pushgate_not_a_link() {
[ ! -L "$1" ] || {
echo "refusing: $1 is a symlink — the agent home must be real directories inside .pushgate, or credentials land wherever it points" >&2; return 1; }
}
pushgate_not_a_link .pushgate || exit 1
pushgate_not_a_link .pushgate/agent-home || exit 1
pushgate_not_a_link .pushgate/agent-home/.config || exit 1
pushgate_not_a_link .pushgate/agent-home/.cache || exit 1
pushgate_not_a_link .pushgate/agent-home/.codex || exit 1
pushgate_not_a_link .pushgate/agent-home/.claude || exit 1
mkdir -p .pushgate/agent-home/.config .pushgate/agent-home/.cache \
.pushgate/agent-home/.codex .pushgate/agent-home/.claude
__ah_real="$(cd -P .pushgate/agent-home 2>/dev/null && pwd -P)"
__pg_real="$(cd -P .pushgate 2>/dev/null && pwd -P)"
[ -n "$__ah_real" ] && [ -n "$__pg_real" ] || {
echo 'refusing: could not canonicalize the agent home, so it cannot be proven to sit inside .pushgate' >&2; exit 1; }
case "$__ah_real" in "$__pg_real"/*) ;; *)
echo "refusing: the agent home resolves outside .pushgate ($__ah_real) — credentials would be written where the ignore rule does not reach" >&2; exit 1 ;; esac
PUSHGATE_GIT="$(pushgate_host_tool git)" || exit 1
"$PUSHGATE_GIT" -c core.fsmonitor= -c core.hooksPath=/dev/null -c core.pager=cat check-ignore -q .pushgate || {
echo 'refusing: .pushgate is not git-ignored: add ".pushgate/" to .gitignore, or to .git/info/exclude to keep it out of your diff' >&2; exit 1; }
[ -z "$("$PUSHGATE_GIT" -c core.fsmonitor= -c core.hooksPath=/dev/null -c core.pager=cat ls-files -- .pushgate)" ] || {
echo 'refusing: .pushgate is tracked — run "git rm -r --cached .pushgate"' >&2; exit 1; }
PUSHGATE_AGENT_STATE= PUSHGATE_AGENT_TMP=
__pg_ws="$(pwd -P)" && __pg_id="$(printf '%s' "$__pg_ws" | $PUSHGATE_SHA256)" \
&& __pg_c="$(getconf DARWIN_USER_CACHE_DIR 2>/dev/null)" && __pg_t="$(getconf DARWIN_USER_TEMP_DIR 2>/dev/null)" \
&& [ -n "$__pg_ws" ] && [ -n "$__pg_c" ] && [ -n "$__pg_t" ] \
&& __pg_c="$(cd -P "$__pg_c" && pwd -P)" && __pg_t="$(cd -P "$__pg_t" && pwd -P)" || {
echo 'refusing: no per-user macOS cache and temp directory (getconf DARWIN_USER_CACHE_DIR, DARWIN_USER_TEMP_DIR), this profile is macOS-only' >&2; exit 1; }
pushgate_private_dir() {
case "$1/" in "$__pg_ws"/*) echo "refusing: the agent state directory $1 is inside the workspace" >&2; return 1 ;; esac
pushgate_not_a_link "$1" || return 1
mkdir -p "$1" && chmod 700 "$1" || { echo "refusing: could not create $1" >&2; return 1; }
}
PUSHGATE_AGENT_STATE="$__pg_c/pushgate-agent-${__pg_id%% *}"
PUSHGATE_AGENT_TMP="$__pg_t/pushgate-agent-${__pg_id%% *}"
pushgate_private_dir "$PUSHGATE_AGENT_STATE" && pushgate_private_dir "$PUSHGATE_AGENT_STATE/cilock" \
&& pushgate_private_dir "$PUSHGATE_AGENT_TMP" || exit 1
echo "agent state: CILOCK_STATE_DIR=$PUSHGATE_AGENT_STATE/cilock, set it on the host to enroll this agent" >&2
__pg_tw="$(pwd -P)" && [ -n "$__pg_tw" ] || {
echo 'refusing: could not resolve the workspace directory' >&2; exit 1; }
__pg_tm="${TMPDIR:-/tmp}"
__pg_tp="$(cd -P "$__pg_tm" 2>/dev/null && pwd -P)" || __pg_tp="$__pg_tm"
pushgate_agent_writable() {
case "$1/" in "${__pg_tw%/}"/*|"${__pg_tm%/}"/*|"${__pg_tp%/}"/*) return 0 ;; esac
__pg_n="${1#/private}"
case "$1/" in /private/*) ;; *) __pg_n="$1" ;; esac
case "$__pg_n/" in /tmp/*|/var/folders/*|/dev/*) return 0 ;; esac
return 1
}
__pg_nl='
'
__pg_r1="$HOME/.config/pushgate" __pg_r2="$HOME/.local/share/pushgate"
pushgate_no_acl() {
case "$1/" in "${__pg_r1%/}"/*|"${__pg_r2%/}"/*) __pg_am=any ;; *) __pg_am=allow ;; esac
if __pg_al="$(ls -lde "$1" 2>/dev/null)"; then
case "$__pg_al" in *"$__pg_nl"*) __pg_al="${__pg_al#*"$__pg_nl"}" ;; *) return 0 ;; esac
if [ "$__pg_am" = allow ]; then
case "$__pg_al" in *" allow"*) ;; *) return 0 ;; esac
fi
elif __pg_al="$(ls -ld "$1" 2>/dev/null)"; then
case "${__pg_al%% *}" in ??????????+*) ;; *) return 0 ;; esac
fi
echo "refusing: $1 carries an access control list -- or its ACL could not be read -- so another account could be granted write to what this boundary trusts" >&2; exit 1
}
pushgate_trusted_path() {
case "$1" in /*) ;; *) echo "refusing: $1 is not an absolute path" >&2; exit 1 ;; esac
__pg_left="${1#/}" __pg_at= __pg_hops=0
while [ -n "$__pg_left" ]; do
__pg_c="${__pg_left%%/*}"
case "$__pg_left" in */*) __pg_left="${__pg_left#*/}" ;; *) __pg_left= ;; esac
[ -n "$__pg_c" ] && [ "$__pg_c" != . ] || continue
[ "$__pg_c" != .. ] || { __pg_at="${__pg_at%/*}"; continue; }
__pg_at="$__pg_at/$__pg_c"
! pushgate_agent_writable "$__pg_at" || {
echo "refusing: $1 passes through $__pg_at, which is inside the workspace or inside a directory a sandboxed agent can write, so the agent could redirect it" >&2; exit 1; }
if [ -e "$__pg_at" ] || [ -L "$__pg_at" ]; then pushgate_no_acl "$__pg_at"; fi
[ -L "$__pg_at" ] || continue
__pg_hops=$((__pg_hops + 1))
__pg_l="$(readlink "$__pg_at")" && [ -n "$__pg_l" ] && [ "$__pg_hops" -le 32 ] || {
echo "refusing: could not resolve the link $__pg_at in $1" >&2; exit 1; }
case "$__pg_l" in /*) __pg_at= ;; *) __pg_at="${__pg_at%/*}" ;; esac
__pg_left="${__pg_l#/}${__pg_left:+/$__pg_left}"
done
case "$__pg_tw/" in "${__pg_at%/}"/*)
echo "refusing: the workspace $__pg_tw is inside $__pg_at ($1), which this boundary trusts" >&2; exit 1 ;; esac
}
pushgate_real_dirs() {
__pg_left="${1#/}" __pg_at=
while [ -n "$__pg_left" ]; do
__pg_c="${__pg_left%%/*}"
case "$__pg_left" in */*) __pg_left="${__pg_left#*/}" ;; *) __pg_left= ;; esac
[ -n "$__pg_c" ] || continue
__pg_at="$__pg_at/$__pg_c"
# Not there yet is not a link; whatever reads it next refuses on its own.
[ -e "$__pg_at" ] || [ -L "$__pg_at" ] || return 0
[ ! -L "$__pg_at" ] && [ -d "$__pg_at" ] || {
echo "refusing: $__pg_at is a symlink or not a directory, and $1 must sit on real directories no sandbox can redirect -- run the setup block above again" >&2; exit 1; }
pushgate_no_acl "$__pg_at"
case "$__pg_at/" in "${HOME%/}"/*) [ -O "$__pg_at" ] || {
echo "refusing: $__pg_at is not owned by you, so another account could rewrite $1" >&2; exit 1; } ;; esac
done
}
pushgate_real_file() {
__pg_f="$1"
pushgate_real_dirs "${__pg_f%/*}"
[ -e "$1" ] || [ -L "$1" ] || return 0
[ -f "$1" ] && [ ! -L "$1" ] || {
echo "refusing: $1 is a symlink or not a regular file -- run the setup block above again" >&2; exit 1; }
pushgate_no_acl "$1"
}
pushgate_trusted_path "$HOME/.config/pushgate"
pushgate_trusted_path "$HOME/.local/share/pushgate"
pushgate_real_dirs "$HOME/.config/pushgate"
pushgate_trusted_path "$HOME/.config/pushgate/srt-macos-deployable.json"
pushgate_trusted_path "$HOME/.local/share/pushgate/srt-0.0.73"
pushgate_trusted_path "$HOME/.local/share/pushgate/srt-0.0.73/node_modules/.bin/srt"
pushgate_real_dirs "$HOME/.local/share/pushgate/srt-0.0.73"
__pg_sb=
read -r __pg_sb < "$HOME/.local/share/pushgate/srt-0.0.73/node_modules/.bin/srt" || true
case "$__pg_sb" in '#!'*)
set -- ${__pg_sb#??}
__pg_ip="$1" __pg_ia="$2"
pushgate_trusted_path "$__pg_ip"
if [ "${__pg_ip##*/}" = env ]; then
__pg_ip="$(PATH="/opt/homebrew/opt/node@22/bin:/opt/homebrew/bin:/usr/local/opt/node@22/bin:/usr/local/bin:/usr/bin:/bin" command -v "$__pg_ia")" && [ -n "$__pg_ip" ] || {
echo "refusing: the Sandbox Runtime interpreter $__pg_ia is not on the launch PATH" >&2; exit 1; }
pushgate_trusted_path "$__pg_ip"
fi ;;
esac
__pg_sr="$(cd -P "$HOME/.local/share/pushgate/srt-0.0.73" 2>/dev/null && pwd -P)" && [ -n "$__pg_sr" ] || {
echo 'refusing: no Sandbox Runtime at $HOME/.local/share/pushgate/srt-0.0.73 -- run the setup block above first' >&2; exit 1; }
__pg_bad="$(find "$__pg_sr" ! -type f ! -type d ! -type l -o ! -user "$(id -u)" -o -type f -links +1 \
-o ! -type l -perm -0020 -o ! -type l -perm -0002 -o -name "*$__pg_nl*")" && [ -z "$__pg_bad" ] || {
echo "refusing: the Sandbox Runtime tree may hold only your own regular files, directories and links, with no hard links and nothing another account can write -- these are not: $__pg_bad" >&2; exit 1; }
if ls -lde "$__pg_sr" >/dev/null 2>&1; then
__pg_ls="$(ls -lRAe "$__pg_sr")" && __pg_re='^ *[0-9][0-9]*: '
else
__pg_ls="$(ls -lRA "$__pg_sr")" && __pg_re='^[^ ][^ ][^ ][^ ][^ ][^ ][^ ][^ ][^ ][^ ][+]'
fi || {
echo 'refusing: could not list the Sandbox Runtime tree to read its access control lists' >&2; exit 1; }
printf '%s\n' "$__pg_ls" | grep -Eq "$__pg_re"
[ $? -eq 1 ] || {
echo "refusing: an entry in the Sandbox Runtime tree carries an access control list -- or its ACLs could not be read -- so another account could be granted write to it" >&2; exit 1; }
pushgate_tree_link() {
__pg_left="$1" __pg_at= __pg_hops=0
while [ -n "$__pg_left" ]; do
__pg_c="${__pg_left%%/*}"
case "$__pg_left" in */*) __pg_left="${__pg_left#*/}" ;; *) __pg_left= ;; esac
[ -n "$__pg_c" ] && [ "$__pg_c" != . ] || continue
if [ "$__pg_c" = .. ]; then
[ -n "$__pg_at" ] || {
echo "refusing: the link $__pg_sr/$1 climbs out of the Sandbox Runtime tree, where another sandbox could rewrite what it reaches" >&2; exit 1; }
__pg_at="${__pg_at%/*}"; continue
fi
__pg_at="$__pg_at/$__pg_c"
[ -L "$__pg_sr$__pg_at" ] || continue
__pg_hops=$((__pg_hops + 1))
__pg_l="$(readlink "$__pg_sr$__pg_at")" && [ -n "$__pg_l" ] && [ "$__pg_hops" -le 40 ] || {
echo "refusing: could not resolve the link $__pg_sr/$1 inside the Sandbox Runtime tree -- it loops or takes more than 40 hops" >&2; exit 1; }
case "$__pg_l" in /*)
echo "refusing: the link $__pg_sr$__pg_at in the Sandbox Runtime tree is absolute ($__pg_l) -- every link in it must be relative and stay inside the tree" >&2; exit 1 ;; esac
__pg_at="${__pg_at%/*}"
__pg_left="$__pg_l${__pg_left:+/$__pg_left}"
done
}
__pg_links="$(find "$__pg_sr" -type l)" || {
echo 'refusing: could not list the links in the Sandbox Runtime tree' >&2; exit 1; }
printf '%s\n' "$__pg_links" | while IFS= read -r __pg_ln; do
[ -z "$__pg_ln" ] || pushgate_tree_link "${__pg_ln#"$__pg_sr"/}"
done || exit 1
pushgate_real_file "$HOME/.config/pushgate/srt-macos-deployable.json"
PUSHGATE_RUN="$(umask 077 && mktemp -d "$HOME/.config/pushgate/launch.XXXXXX")" && [ -n "$PUSHGATE_RUN" ] || {
echo 'refusing: could not create a private launch directory' >&2; exit 1; }
trap 'rm -rf "$PUSHGATE_RUN"' EXIT
pushgate_trusted_path "$PUSHGATE_RUN"
pushgate_real_dirs "$PUSHGATE_RUN"
cp "$HOME/.config/pushgate/srt-macos-deployable.json" "$PUSHGATE_RUN/srt-macos-deployable.json" || {
echo 'refusing: could not make a private copy of the deployable Sandbox Runtime settings' >&2; exit 1; }
pushgate_no_acl "$PUSHGATE_RUN/srt-macos-deployable.json"
printf '%s %s\n' '280886bfdd9e3f1eded02c6b544454e337a4bd59e5d2e944771efdbaf597b1af' "$PUSHGATE_RUN/srt-macos-deployable.json" | $PUSHGATE_SHA256 -c - >/dev/null 2>&1 || {
echo 'refusing: the installed deployable Sandbox Runtime settings is not the one this page publishes -- run the setup block above again' >&2; exit 1; }
env -i HOME="$PWD/.pushgate/agent-home" \
XDG_CONFIG_HOME="$PWD/.pushgate/agent-home/.config" \
CODEX_HOME="$PWD/.pushgate/agent-home/.codex" \
CLAUDE_CONFIG_DIR="$PWD/.pushgate/agent-home/.claude" \
PATH="/opt/homebrew/opt/node@22/bin:/opt/homebrew/bin:/usr/local/opt/node@22/bin:/usr/local/bin:/usr/bin:/bin" \
TERM="${TERM:-xterm-256color}" \
CILOCK_STATE_DIR="$PUSHGATE_AGENT_STATE/cilock" \
GOCACHE="$PUSHGATE_AGENT_STATE/go-build" \
GOMODCACHE="$PUSHGATE_AGENT_STATE/go-mod" \
CLAUDE_CODE_TMPDIR="$PUSHGATE_AGENT_TMP" \
"$HOME/.local/share/pushgate/srt-0.0.73/node_modules/.bin/srt" \
--settings "$PUSHGATE_RUN/srt-macos-deployable.json" -- codex --sandbox danger-full-access --ask-for-approval on-request
On macOS, copy the block, then run bash <(pbpaste) in your terminal. Pasting the block itself fails in the default macOS shells.
Then, before the agent signs, run these on the host in the same repository, substituting the path the block printed:
export CILOCK_STATE_DIR=<printed path>
cilock enroll agent --repo OWNER/REPO
cilock git configure
They run on the host because enrollment needs the loopback callback and cilock git configure writes .git/config, and this profile allows neither. The agent inside uses the same state, so it signs as the identity you enrolled.
For Claude Code or Cursor, replace codex --sandbox danger-full-access --ask-for-approval on-request on the last line with claude --model <a current model id> or agent. On a fresh configuration Claude Code can default to a model id that returns 404.
Claude Code also cannot read the macOS Keychain inside the sandbox, so it asks you to sign in once and keeps the credential in .pushgate/agent-home/.claude/.credentials.json, which the guard keeps git-ignored. Exclude .pushgate/** from every tree scan, such as a secret scan or a material attestor, because the agent's model credentials live there.
Linux in a Colima or Docker container
Container boundary
This containment-only fixture runs without the host network, host home, Docker socket, SSH agent socket, or added capabilities. Mount only the repository.
The command below writes the controls Dockerfile into a private temporary directory of its own, builds the fixture there from a digest-pinned Debian base, and opens an interactive shell inside the boundary so you can inspect its controls. It needs no file from this repository and writes nothing into yours.
Its disabled network also blocks the cumulative ALPS 1 signing path, so it does not by itself satisfy ALPS 2. Coding agents cannot run inside this fixture: the image installs no agent CLI, and the disabled network blocks model-endpoint egress.
A deployable Linux agent composition needs an agent-bearing digest-pinned image, an approved egress allowlist, and an independently mediated CI/lock path with narrow platform, Fulcio, and TSA access.
# ALPS 0.1 TRUSTED BOOTSTRAP — the first thing this block does, before any command runs.
# PATH is pinned to the base system, so every utility below (env, find, mkdir, mktemp, ...)
# is the host's own and never this repository's. The PATH you had is kept aside for two
# uses only: LOCATING the third-party tools this block then verifies, and handing a
# verified tool the PATH its own helpers expect. A second block pasted into the same
# shell keeps the PATH the first one saved, rather than saving the pin as if it were yours.
PUSHGATE_AMBIENT_PATH="${PUSHGATE_AMBIENT_PATH:-$PATH}"
export PATH='/usr/bin:/bin'
# SAME-UID LIMIT — what the checks below do, and what they cannot do.
# pushgate_host_tool refuses a tool supplied from inside this workspace, and refuses one whose
# file or parent directory is world-writable. That catches a repository-controlled PATH entry and
# a binary any OTHER local user could have replaced. It does NOT establish that the tool is
# genuine. Someone already running as YOUR user can write $HOME/bin/nono or $HOME/bin/gh with
# ordinary 0755 permissions: outside the workspace, not world-writable, so it passes every check
# here and is then invoked as the "verified" tool. A fake 'gh attestation verify' exits 0 and a
# fake 'nono' prints the expected version, and the agent is recorded as sandboxed and launched
# with no sandbox at all. This applies to every tool resolved below, cosign, curl and cilock
# included, and to the attestation check that verifies nono.
# No file-permission test can close this: the check runs with exactly the privileges of the
# process it would have to catch. The mitigation is a different privilege domain, not a stronger
# probe — run the agent under a SEPARATE UID, in a container, or in a VM, so that writing
# $HOME/bin is not something it can do in the first place.
#
# GROUP-WRITE, separately, IS yours to close. These checks test the other-write bit and not the
# group-write bit, and Homebrew's default prefix is group-writable: measured on Apple silicon,
# /opt/homebrew/bin is 'drwxrwxr-x <user>:admin' (Intel /usr/local/bin is the same). So any other
# member of that group can replace nono, gh, cosign or cilock before confinement starts and these
# checks still report host-owned. That is a different principal from you, unlike the case above.
# It is not rejected automatically because rejecting it refuses every Homebrew-installed tool,
# which is the install path this guide recommends. If anyone else has admin on this machine,
# close it yourself: chmod g-w /opt/homebrew/bin, or install these tools outside a group-writable
# prefix.
#
# OWNERSHIP ASSUMPTION, and its cost. A resolved tool is accepted only when YOU or ROOT own it,
# because any other owner can rewrite those bytes whenever they like. The cost lands on honest
# setups: a tool owned by a SERVICE ACCOUNT -- a CI runner toolchain under a build user, a
# linuxbrew prefix, a vendored toolchain installed as its own daemon user -- is refused here even
# though nobody is attacking. If that is your machine, it is a stated limit of this check rather than
# a finding: install or resolve the tool from a path owned by you or by root. It is deliberately
# not widened by guessing at a system-uid threshold, because macOS numbers its first human user
# 501 and Linux reserves under 1000 for daemons, so any single threshold would either trust a
# human peer on macOS or refuse a service account on Linux.
pushgate_host_has() { (PATH="$PUSHGATE_AMBIENT_PATH"; command -v "$1") >/dev/null 2>&1; }
pushgate_world_writable() {
__wwl="$(ls -ld "$1" 2>/dev/null)" || return 2
set -- $__wwl
[ $# -ge 3 ] || return 2
__wwo="$3"
__wwl="$1"
case "$__wwl" in ??????????*) ;; *) return 2 ;; esac
# WHO THIS PROCESS IS, resolved BEFORE any branch may answer "trusted".
# Every acceptance below is a statement about the owner, so an acceptance
# reached without this value is an acceptance made without asking the
# question. Recomputed on every call rather than cached across the walk:
# caching would let a __wwme already set in the pasted shell's environment
# decide who this process is, which hands the same-UID attacker a fresh lever
# to buy back a few forks the walk would never have noticed.
__wwme="$(id -un 2>/dev/null)" || return 2
[ -n "$__wwme" ] || return 2
# A STICKY directory is world-writable but restricted-deletion: anyone may
# create their own entries, and only the owner of an existing entry may
# unlink or rename it. That is precisely the question here — can someone
# else swap these bytes — so /tmp and friends are NOT replaceable. Without
# this, the ancestor walk below rejects every tool under /tmp on Linux,
# where TMPDIR is /tmp (drwxrwxrwt); macOS hides the bug because TMPDIR is a
# private per-user /var/folders path. CI caught exactly that difference.
#
# RESTRICTED-DELETION DOES NOT BIND THE DIRECTORY'S OWN OWNER. Sticky stops a
# STRANGER from unlinking somebody else's entry; the owner of the directory
# may still rename or remove anything inside it. So the sticky bit answers
# "can a stranger swap this" and says nothing about the owner, and a 1777
# directory belonging to another principal can replace the verified staging
# subtree between the integrity check and the bash that runs it. That is
# this file's own defect one branch further in — a mode bit accepted as an
# answer to a question about ownership — so the sticky branch asks the same
# question every other acceptance asks instead of returning early.
case "$__wwl" in d????????[tT]*)
case "$__wwo" in "$__wwme"|'root') return 1 ;; *) return 0 ;; esac ;;
esac
__wwl="${__wwl#????????}"
case "$__wwl" in w*) return 0 ;; -*) ;; *) return 2 ;; esac
# WHO OWNS IT, not just who else can write it. A 0755 file has its other-write
# bit clear and is still rewritable at will BY ITS OWNER, so a tool owned by
# another local user was being accepted as host-owned on the strength of a bit
# that says nothing about them. Same defect as checking a directory without its
# ancestors: the thing was validated and its context was not.
#
# Only this user and root qualify. Root because the base system is not
# installed by the reader; this user because that is the same-UID boundary
# stated below, which no file mode can close. Any other owner is a different
# principal who can rewrite these bytes whenever they like.
#
# THE FALSE POSITIVE THIS BUYS, NAMED RATHER THAN DISCOVERED. A tool owned by
# a SERVICE ACCOUNT -- a Linux runner's toolchain under a build user, a
# linuxbrew prefix, a vendored toolchain installed as its own daemon user --
# is refused here even though nobody is attacking. That is a real cost and it
# falls on honest configurations, which is the failure mode that gets a
# security control deleted rather than fixed, so it is stated in the block the
# reader pastes and the refusal below says what to do about it.
#
# It is not auto-widened, and the reason is that the obvious widening does not
# survive contact: 'trust uids below the system threshold' needs a threshold,
# and there is no single one. macOS numbers its first human user 501 while
# Linux reserves everything under 1000 for system accounts, so any constant
# either trusts a macOS human being or refuses a Linux service account. A
# heuristic that guesses wrong in the first direction is worse than a refusal
# a reader can read and act on.
case "$__wwo" in "$__wwme"|'root') return 1 ;; *) return 0 ;; esac
}
# Every directory from a path up to / — a writable ancestor lets a principal
# rename the directory below it and substitute the whole subtree, so checking
# only the immediate parent leaves the same replacement one level up.
pushgate_world_writable_path() {
__wwp="$1"
while : ; do
pushgate_world_writable "${__wwp:-/}"; __wwr=$?
[ "$__wwr" -eq 1 ] || return "$__wwr"
case "$__wwp" in ""|"/") return 1 ;; esac
__wwp="${__wwp%/*}"
done
}
pushgate_host_stage() {
__stage="$(mktemp -d)" || {
echo 'refusing: could not create a private staging directory' >&2; return 1; }
[ -n "$__stage" ] || {
echo 'refusing: empty staging directory path' >&2; return 1; }
__stage="$(cd -P "$__stage" 2>/dev/null && pwd -P)"
[ -n "$__stage" ] || {
echo 'refusing: could not canonicalize the staging directory, so it cannot be proven outside the workspace' >&2; return 1; }
__stage_pwd="$(cd -P "$PWD" 2>/dev/null && pwd -P)"
[ -n "$__stage_pwd" ] || {
echo 'refusing: cannot resolve the workspace directory, so nothing can be proven outside it' >&2; return 1; }
case "$__stage" in "$__stage_pwd"|"$__stage_pwd"/*)
echo "refusing: TMPDIR stages inside the workspace ($__stage) — verified bytes would sit where the agent this boundary contains can rewrite them" >&2; return 1 ;; esac
# THE WHOLE CHAIN, not the leaf. mktemp -d makes a 0700 directory, and that
# says nothing about the directories above it: a non-sticky world-writable
# ancestor lets another user rename this one away and substitute their own
# between the integrity check and the bash that runs the result. The
# ancestor walk is the same one the resolved tool paths get.
pushgate_world_writable_path "$__stage"; __stage_ww=$?
[ "$__stage_ww" -eq 1 ] || {
echo "refusing: the staging directory, or a directory above it, is world-writable, owned by another principal, or could not be read" >&2; return 1; }
printf '%s\n' "$__stage"
}
pushgate_host_tool() {
__invoke="$(PATH="$PUSHGATE_AMBIENT_PATH"; command -v "$1" 2>/dev/null || true)"
case "$__invoke" in /*) ;; *)
echo "refusing: no host-owned $1 on PATH" >&2; return 1 ;; esac
__tool="$__invoke"
__hops=0
while [ -L "$__tool" ] && [ "$__hops" -lt 16 ]; do
__link="$(readlink "$__tool")"
case "$__link" in
/*) __tool="$__link" ;;
*) __tool="${__tool%/*}/$__link" ;;
esac
__hops=$((__hops + 1))
done
__dir="${__tool%/*}"
__tool="$(cd -P "${__dir:-/}" 2>/dev/null && pwd -P)/${__tool##*/}"
__dir="${__invoke%/*}"
__invoke="$(cd -P "${__dir:-/}" 2>/dev/null && pwd -P)/${__invoke##*/}"
__invoke_dir="${__invoke%/*}"
__pwd="$(cd -P "$PWD" 2>/dev/null && pwd -P)"
[ -n "$__pwd" ] || {
echo "refusing: cannot resolve the workspace directory, so nothing can be excluded from it" >&2; return 1; }
case "$__tool" in "$__pwd"/*)
echo "refusing: $1 resolves inside the workspace ($__tool) — a repository-controlled PATH entry cannot supply it" >&2; return 1 ;; esac
case "$__invoke" in "$__pwd"/*)
echo "refusing: $1 is supplied from inside the workspace ($__invoke) — a repository-controlled PATH entry cannot supply it" >&2; return 1 ;; esac
[ -x "$__tool" ] || { echo "refusing: $__tool is not executable" >&2; return 1; }
pushgate_world_writable "$__tool"; __ww=$?
[ "$__ww" -eq 1 ] || {
echo "refusing: $__tool is world-writable, owned by a user other than you or root, or its permissions could not be read, so it is not provably host-owned. If it is owned by a service account you trust -- a runner toolchain, a linuxbrew prefix -- that is a stated limitation of this check, not an attack: install or resolve the tool from a path owned by you or by root." >&2; return 1; }
# THE RESOLVED TARGET'S OWN DIRECTORY CHAIN, not just the invocation's.
# A symlink on a tight path pointing into a writable directory was accepted:
# the link and its directory both looked fine while the bytes it named could
# be replaced at will. The file's mode says nothing about that — replacing a
# file needs write on its DIRECTORY, so the directory is the thing to check.
pushgate_world_writable_path "${__tool%/*}"; __ww=$?
[ "$__ww" -eq 1 ] || {
echo "refusing: the directory holding $__tool is world-writable, or its permissions could not be read, so $1 could be replaced there" >&2; return 1; }
pushgate_world_writable_path "${__invoke_dir:-/}"; __ww=$?
[ "$__ww" -eq 1 ] || {
echo "refusing: ${__invoke_dir:-/} is world-writable, or its permissions could not be read, so $1 could be repointed" >&2; return 1; }
printf '%s\n' "$__invoke"
}
PUSHGATE_DOCKER="$(pushgate_host_tool docker)" || exit 1
CONTROLS_IMAGE_ID= CONTROLS_CONTEXT_DIR="$(pushgate_host_stage)" || {
echo 'refusing: could not create a private build context' >&2; exit 1; }
[ -n "$CONTROLS_CONTEXT_DIR" ] || {
echo 'refusing: empty build context path' >&2; exit 1; }
if pushgate_host_has shasum; then
PUSHGATE_SHA256="$(pushgate_host_tool shasum)" || exit 1
PUSHGATE_SHA256="$PUSHGATE_SHA256 -a 256"
else PUSHGATE_SHA256="$(pushgate_host_tool sha256sum)" || exit 1; fi
cat > "$CONTROLS_CONTEXT_DIR/Dockerfile.controls" <<'PUSHGATE_CONTROLS_DOCKERFILE'
# The runtime flags under test are documented in /docs/agent-sandbox. This
# fixture adds only Git and OpenSSH so the negative signed-commit control can
# run. Base image digest observed from Docker's registry on 2026-08-25.
FROM debian@sha256:b1a741487078b369e78119849663d7f1a5341ef2768798f7b7406c4240f86aef
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates git netcat-openbsd openssh-client \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /workspace
PUSHGATE_CONTROLS_DOCKERFILE
printf '%s %s\n' '9318bcae6a23789ac8dec26afaf9ad6587c6e09a6d5f6cf127662eea3d03d341' "$CONTROLS_CONTEXT_DIR/Dockerfile.controls" | $PUSHGATE_SHA256 -c - \
|| { rm -f "$CONTROLS_CONTEXT_DIR/Dockerfile.controls"; \
echo 'refusing: the pasted block did not survive the copy — paste it again' >&2; exit 1; }
CONTROLS_BUILD_TAG="pushgate-agent-sandbox-controls:${CONTROLS_CONTEXT_DIR##*/}" \
&& PATH="$PUSHGATE_AMBIENT_PATH" "$PUSHGATE_DOCKER" build --provenance=false --sbom=false -t "$CONTROLS_BUILD_TAG" \
-f "$CONTROLS_CONTEXT_DIR/Dockerfile.controls" \
"$CONTROLS_CONTEXT_DIR" \
&& CONTROLS_IMAGE_ID="$("$PUSHGATE_DOCKER" image inspect "$CONTROLS_BUILD_TAG" \
--format '{{.Id}}')" \
&& "$PUSHGATE_DOCKER" run --rm -it --read-only --network none \
--cap-drop ALL --security-opt no-new-privileges --pids-limit 512 \
--user "$(id -u):$(id -g)" --env HOME=/tmp/agent-home \
--tmpfs /tmp:rw,nosuid,nodev,noexec,size=256m \
--mount type=bind,src="$PWD",dst=/workspace --workdir /workspace \
"$CONTROLS_IMAGE_ID" /bin/sh
On macOS, copy the block, then run bash <(pbpaste) in your terminal. Pasting the block itself fails in the default macOS shells.
The container receives a fresh in-memory home. Do not add -v $HOME, -v /var/run/docker.sock, --privileged, --network host, or an SSH_AUTH_SOCK mount.
Four things in that block are load-bearing:
- It launches a digest, never a tag. The launch reference is the image's config digest, held in the
$CONTROLS_IMAGE_IDshell variable. The build tag is only a handle for thedocker image inspectthat resolves it. - It writes nothing into your workspace. A predictable workspace path is one the agent you are boxing in can write: it could pre-create that path as a symlink to redirect the write, or swap the value before
docker runreads it back. A shell variable has neither weakness. The build context is a freshmktemp -dfor the same reason. A context inside your repository is a context the boxed agent can edit, and the Dockerfile it edits is the image that then gets launched over that repository. - Every step is chained with
&&. A failed build never reaches the inspect, and a failed inspect never reachesdocker run. - The flags come from one definition. Every isolation flag above is rendered from a single definition in the page source rather than retyped, so the command shown here and the command any check of it reproduces are the same string.
--provenance=false --sbom=false is what makes the digest reproducible. Without those flags BuildKit wraps each build in a per-invocation OCI image index, and the digest changes every time even when the layers and image config are byte-identical.
So two builds of these same inputs on the same platform produce the same digest. A build on a different architecture is a different image by construction, and the apt packages are unversioned, so a build made after Debian publishes new packages also legitimately differs. Either way a mismatch means different bytes, not a broken check.
Launch the coding agent
Choose your coding agent. Every launch below starts it inside one of the documented macOS boundaries, which keep signing authority outside the agent process. The Linux container fixture hosts no coding agent. Run the matching setup from the boundary section above first.
# ALPS 0.1 TRUSTED BOOTSTRAP — the first thing this block does, before any command runs.
# PATH is pinned to the base system, so every utility below (env, find, mkdir, mktemp, ...)
# is the host's own and never this repository's. The PATH you had is kept aside for two
# uses only: LOCATING the third-party tools this block then verifies, and handing a
# verified tool the PATH its own helpers expect. A second block pasted into the same
# shell keeps the PATH the first one saved, rather than saving the pin as if it were yours.
PUSHGATE_AMBIENT_PATH="${PUSHGATE_AMBIENT_PATH:-$PATH}"
export PATH='/usr/bin:/bin'
# SAME-UID LIMIT — what the checks below do, and what they cannot do.
# pushgate_host_tool refuses a tool supplied from inside this workspace, and refuses one whose
# file or parent directory is world-writable. That catches a repository-controlled PATH entry and
# a binary any OTHER local user could have replaced. It does NOT establish that the tool is
# genuine. Someone already running as YOUR user can write $HOME/bin/nono or $HOME/bin/gh with
# ordinary 0755 permissions: outside the workspace, not world-writable, so it passes every check
# here and is then invoked as the "verified" tool. A fake 'gh attestation verify' exits 0 and a
# fake 'nono' prints the expected version, and the agent is recorded as sandboxed and launched
# with no sandbox at all. This applies to every tool resolved below, cosign, curl and cilock
# included, and to the attestation check that verifies nono.
# No file-permission test can close this: the check runs with exactly the privileges of the
# process it would have to catch. The mitigation is a different privilege domain, not a stronger
# probe — run the agent under a SEPARATE UID, in a container, or in a VM, so that writing
# $HOME/bin is not something it can do in the first place.
#
# GROUP-WRITE, separately, IS yours to close. These checks test the other-write bit and not the
# group-write bit, and Homebrew's default prefix is group-writable: measured on Apple silicon,
# /opt/homebrew/bin is 'drwxrwxr-x <user>:admin' (Intel /usr/local/bin is the same). So any other
# member of that group can replace nono, gh, cosign or cilock before confinement starts and these
# checks still report host-owned. That is a different principal from you, unlike the case above.
# It is not rejected automatically because rejecting it refuses every Homebrew-installed tool,
# which is the install path this guide recommends. If anyone else has admin on this machine,
# close it yourself: chmod g-w /opt/homebrew/bin, or install these tools outside a group-writable
# prefix.
#
# OWNERSHIP ASSUMPTION, and its cost. A resolved tool is accepted only when YOU or ROOT own it,
# because any other owner can rewrite those bytes whenever they like. The cost lands on honest
# setups: a tool owned by a SERVICE ACCOUNT -- a CI runner toolchain under a build user, a
# linuxbrew prefix, a vendored toolchain installed as its own daemon user -- is refused here even
# though nobody is attacking. If that is your machine, it is a stated limit of this check rather than
# a finding: install or resolve the tool from a path owned by you or by root. It is deliberately
# not widened by guessing at a system-uid threshold, because macOS numbers its first human user
# 501 and Linux reserves under 1000 for daemons, so any single threshold would either trust a
# human peer on macOS or refuse a service account on Linux.
pushgate_host_has() { (PATH="$PUSHGATE_AMBIENT_PATH"; command -v "$1") >/dev/null 2>&1; }
pushgate_world_writable() {
__wwl="$(ls -ld "$1" 2>/dev/null)" || return 2
set -- $__wwl
[ $# -ge 3 ] || return 2
__wwo="$3"
__wwl="$1"
case "$__wwl" in ??????????*) ;; *) return 2 ;; esac
# WHO THIS PROCESS IS, resolved BEFORE any branch may answer "trusted".
# Every acceptance below is a statement about the owner, so an acceptance
# reached without this value is an acceptance made without asking the
# question. Recomputed on every call rather than cached across the walk:
# caching would let a __wwme already set in the pasted shell's environment
# decide who this process is, which hands the same-UID attacker a fresh lever
# to buy back a few forks the walk would never have noticed.
__wwme="$(id -un 2>/dev/null)" || return 2
[ -n "$__wwme" ] || return 2
# A STICKY directory is world-writable but restricted-deletion: anyone may
# create their own entries, and only the owner of an existing entry may
# unlink or rename it. That is precisely the question here — can someone
# else swap these bytes — so /tmp and friends are NOT replaceable. Without
# this, the ancestor walk below rejects every tool under /tmp on Linux,
# where TMPDIR is /tmp (drwxrwxrwt); macOS hides the bug because TMPDIR is a
# private per-user /var/folders path. CI caught exactly that difference.
#
# RESTRICTED-DELETION DOES NOT BIND THE DIRECTORY'S OWN OWNER. Sticky stops a
# STRANGER from unlinking somebody else's entry; the owner of the directory
# may still rename or remove anything inside it. So the sticky bit answers
# "can a stranger swap this" and says nothing about the owner, and a 1777
# directory belonging to another principal can replace the verified staging
# subtree between the integrity check and the bash that runs it. That is
# this file's own defect one branch further in — a mode bit accepted as an
# answer to a question about ownership — so the sticky branch asks the same
# question every other acceptance asks instead of returning early.
case "$__wwl" in d????????[tT]*)
case "$__wwo" in "$__wwme"|'root') return 1 ;; *) return 0 ;; esac ;;
esac
__wwl="${__wwl#????????}"
case "$__wwl" in w*) return 0 ;; -*) ;; *) return 2 ;; esac
# WHO OWNS IT, not just who else can write it. A 0755 file has its other-write
# bit clear and is still rewritable at will BY ITS OWNER, so a tool owned by
# another local user was being accepted as host-owned on the strength of a bit
# that says nothing about them. Same defect as checking a directory without its
# ancestors: the thing was validated and its context was not.
#
# Only this user and root qualify. Root because the base system is not
# installed by the reader; this user because that is the same-UID boundary
# stated below, which no file mode can close. Any other owner is a different
# principal who can rewrite these bytes whenever they like.
#
# THE FALSE POSITIVE THIS BUYS, NAMED RATHER THAN DISCOVERED. A tool owned by
# a SERVICE ACCOUNT -- a Linux runner's toolchain under a build user, a
# linuxbrew prefix, a vendored toolchain installed as its own daemon user --
# is refused here even though nobody is attacking. That is a real cost and it
# falls on honest configurations, which is the failure mode that gets a
# security control deleted rather than fixed, so it is stated in the block the
# reader pastes and the refusal below says what to do about it.
#
# It is not auto-widened, and the reason is that the obvious widening does not
# survive contact: 'trust uids below the system threshold' needs a threshold,
# and there is no single one. macOS numbers its first human user 501 while
# Linux reserves everything under 1000 for system accounts, so any constant
# either trusts a macOS human being or refuses a Linux service account. A
# heuristic that guesses wrong in the first direction is worse than a refusal
# a reader can read and act on.
case "$__wwo" in "$__wwme"|'root') return 1 ;; *) return 0 ;; esac
}
# Every directory from a path up to / — a writable ancestor lets a principal
# rename the directory below it and substitute the whole subtree, so checking
# only the immediate parent leaves the same replacement one level up.
pushgate_world_writable_path() {
__wwp="$1"
while : ; do
pushgate_world_writable "${__wwp:-/}"; __wwr=$?
[ "$__wwr" -eq 1 ] || return "$__wwr"
case "$__wwp" in ""|"/") return 1 ;; esac
__wwp="${__wwp%/*}"
done
}
pushgate_host_stage() {
__stage="$(mktemp -d)" || {
echo 'refusing: could not create a private staging directory' >&2; return 1; }
[ -n "$__stage" ] || {
echo 'refusing: empty staging directory path' >&2; return 1; }
__stage="$(cd -P "$__stage" 2>/dev/null && pwd -P)"
[ -n "$__stage" ] || {
echo 'refusing: could not canonicalize the staging directory, so it cannot be proven outside the workspace' >&2; return 1; }
__stage_pwd="$(cd -P "$PWD" 2>/dev/null && pwd -P)"
[ -n "$__stage_pwd" ] || {
echo 'refusing: cannot resolve the workspace directory, so nothing can be proven outside it' >&2; return 1; }
case "$__stage" in "$__stage_pwd"|"$__stage_pwd"/*)
echo "refusing: TMPDIR stages inside the workspace ($__stage) — verified bytes would sit where the agent this boundary contains can rewrite them" >&2; return 1 ;; esac
# THE WHOLE CHAIN, not the leaf. mktemp -d makes a 0700 directory, and that
# says nothing about the directories above it: a non-sticky world-writable
# ancestor lets another user rename this one away and substitute their own
# between the integrity check and the bash that runs the result. The
# ancestor walk is the same one the resolved tool paths get.
pushgate_world_writable_path "$__stage"; __stage_ww=$?
[ "$__stage_ww" -eq 1 ] || {
echo "refusing: the staging directory, or a directory above it, is world-writable, owned by another principal, or could not be read" >&2; return 1; }
printf '%s\n' "$__stage"
}
pushgate_host_tool() {
__invoke="$(PATH="$PUSHGATE_AMBIENT_PATH"; command -v "$1" 2>/dev/null || true)"
case "$__invoke" in /*) ;; *)
echo "refusing: no host-owned $1 on PATH" >&2; return 1 ;; esac
__tool="$__invoke"
__hops=0
while [ -L "$__tool" ] && [ "$__hops" -lt 16 ]; do
__link="$(readlink "$__tool")"
case "$__link" in
/*) __tool="$__link" ;;
*) __tool="${__tool%/*}/$__link" ;;
esac
__hops=$((__hops + 1))
done
__dir="${__tool%/*}"
__tool="$(cd -P "${__dir:-/}" 2>/dev/null && pwd -P)/${__tool##*/}"
__dir="${__invoke%/*}"
__invoke="$(cd -P "${__dir:-/}" 2>/dev/null && pwd -P)/${__invoke##*/}"
__invoke_dir="${__invoke%/*}"
__pwd="$(cd -P "$PWD" 2>/dev/null && pwd -P)"
[ -n "$__pwd" ] || {
echo "refusing: cannot resolve the workspace directory, so nothing can be excluded from it" >&2; return 1; }
case "$__tool" in "$__pwd"/*)
echo "refusing: $1 resolves inside the workspace ($__tool) — a repository-controlled PATH entry cannot supply it" >&2; return 1 ;; esac
case "$__invoke" in "$__pwd"/*)
echo "refusing: $1 is supplied from inside the workspace ($__invoke) — a repository-controlled PATH entry cannot supply it" >&2; return 1 ;; esac
[ -x "$__tool" ] || { echo "refusing: $__tool is not executable" >&2; return 1; }
pushgate_world_writable "$__tool"; __ww=$?
[ "$__ww" -eq 1 ] || {
echo "refusing: $__tool is world-writable, owned by a user other than you or root, or its permissions could not be read, so it is not provably host-owned. If it is owned by a service account you trust -- a runner toolchain, a linuxbrew prefix -- that is a stated limitation of this check, not an attack: install or resolve the tool from a path owned by you or by root." >&2; return 1; }
# THE RESOLVED TARGET'S OWN DIRECTORY CHAIN, not just the invocation's.
# A symlink on a tight path pointing into a writable directory was accepted:
# the link and its directory both looked fine while the bytes it named could
# be replaced at will. The file's mode says nothing about that — replacing a
# file needs write on its DIRECTORY, so the directory is the thing to check.
pushgate_world_writable_path "${__tool%/*}"; __ww=$?
[ "$__ww" -eq 1 ] || {
echo "refusing: the directory holding $__tool is world-writable, or its permissions could not be read, so $1 could be replaced there" >&2; return 1; }
pushgate_world_writable_path "${__invoke_dir:-/}"; __ww=$?
[ "$__ww" -eq 1 ] || {
echo "refusing: ${__invoke_dir:-/} is world-writable, or its permissions could not be read, so $1 could be repointed" >&2; return 1; }
printf '%s\n' "$__invoke"
}
__pg_tw="$(pwd -P)" && [ -n "$__pg_tw" ] || {
echo 'refusing: could not resolve the workspace directory' >&2; exit 1; }
__pg_tm="${TMPDIR:-/tmp}"
__pg_tp="$(cd -P "$__pg_tm" 2>/dev/null && pwd -P)" || __pg_tp="$__pg_tm"
pushgate_agent_writable() {
case "$1/" in "${__pg_tw%/}"/*|"${__pg_tm%/}"/*|"${__pg_tp%/}"/*) return 0 ;; esac
__pg_n="${1#/private}"
case "$1/" in /private/*) ;; *) __pg_n="$1" ;; esac
case "$__pg_n/" in /tmp/*|/var/folders/*|/dev/*) return 0 ;; esac
return 1
}
__pg_nl='
'
__pg_r1="$HOME/.config/pushgate" __pg_r2="$HOME/.local/share/pushgate"
pushgate_no_acl() {
case "$1/" in "${__pg_r1%/}"/*|"${__pg_r2%/}"/*) __pg_am=any ;; *) __pg_am=allow ;; esac
if __pg_al="$(ls -lde "$1" 2>/dev/null)"; then
case "$__pg_al" in *"$__pg_nl"*) __pg_al="${__pg_al#*"$__pg_nl"}" ;; *) return 0 ;; esac
if [ "$__pg_am" = allow ]; then
case "$__pg_al" in *" allow"*) ;; *) return 0 ;; esac
fi
elif __pg_al="$(ls -ld "$1" 2>/dev/null)"; then
case "${__pg_al%% *}" in ??????????+*) ;; *) return 0 ;; esac
fi
echo "refusing: $1 carries an access control list -- or its ACL could not be read -- so another account could be granted write to what this boundary trusts" >&2; exit 1
}
pushgate_trusted_path() {
case "$1" in /*) ;; *) echo "refusing: $1 is not an absolute path" >&2; exit 1 ;; esac
__pg_left="${1#/}" __pg_at= __pg_hops=0
while [ -n "$__pg_left" ]; do
__pg_c="${__pg_left%%/*}"
case "$__pg_left" in */*) __pg_left="${__pg_left#*/}" ;; *) __pg_left= ;; esac
[ -n "$__pg_c" ] && [ "$__pg_c" != . ] || continue
[ "$__pg_c" != .. ] || { __pg_at="${__pg_at%/*}"; continue; }
__pg_at="$__pg_at/$__pg_c"
! pushgate_agent_writable "$__pg_at" || {
echo "refusing: $1 passes through $__pg_at, which is inside the workspace or inside a directory a sandboxed agent can write, so the agent could redirect it" >&2; exit 1; }
if [ -e "$__pg_at" ] || [ -L "$__pg_at" ]; then pushgate_no_acl "$__pg_at"; fi
[ -L "$__pg_at" ] || continue
__pg_hops=$((__pg_hops + 1))
__pg_l="$(readlink "$__pg_at")" && [ -n "$__pg_l" ] && [ "$__pg_hops" -le 32 ] || {
echo "refusing: could not resolve the link $__pg_at in $1" >&2; exit 1; }
case "$__pg_l" in /*) __pg_at= ;; *) __pg_at="${__pg_at%/*}" ;; esac
__pg_left="${__pg_l#/}${__pg_left:+/$__pg_left}"
done
case "$__pg_tw/" in "${__pg_at%/}"/*)
echo "refusing: the workspace $__pg_tw is inside $__pg_at ($1), which this boundary trusts" >&2; exit 1 ;; esac
}
pushgate_real_dirs() {
__pg_left="${1#/}" __pg_at=
while [ -n "$__pg_left" ]; do
__pg_c="${__pg_left%%/*}"
case "$__pg_left" in */*) __pg_left="${__pg_left#*/}" ;; *) __pg_left= ;; esac
[ -n "$__pg_c" ] || continue
__pg_at="$__pg_at/$__pg_c"
# Not there yet is not a link; whatever reads it next refuses on its own.
[ -e "$__pg_at" ] || [ -L "$__pg_at" ] || return 0
[ ! -L "$__pg_at" ] && [ -d "$__pg_at" ] || {
echo "refusing: $__pg_at is a symlink or not a directory, and $1 must sit on real directories no sandbox can redirect -- run the setup block above again" >&2; exit 1; }
pushgate_no_acl "$__pg_at"
case "$__pg_at/" in "${HOME%/}"/*) [ -O "$__pg_at" ] || {
echo "refusing: $__pg_at is not owned by you, so another account could rewrite $1" >&2; exit 1; } ;; esac
done
}
pushgate_real_file() {
__pg_f="$1"
pushgate_real_dirs "${__pg_f%/*}"
[ -e "$1" ] || [ -L "$1" ] || return 0
[ -f "$1" ] && [ ! -L "$1" ] || {
echo "refusing: $1 is a symlink or not a regular file -- run the setup block above again" >&2; exit 1; }
pushgate_no_acl "$1"
}
pushgate_trusted_path "$HOME/.config/pushgate"
pushgate_trusted_path "$HOME/.local/share/pushgate"
pushgate_real_dirs "$HOME/.config/pushgate"
pushgate_trusted_path "$HOME/.config/pushgate/nono.path"
pushgate_trusted_path "$HOME/.config/pushgate/nono.sha256"
pushgate_trusted_path "$HOME/.config/pushgate/nono-macos.json"
pushgate_real_file "$HOME/.config/pushgate/nono.path"
pushgate_real_file "$HOME/.config/pushgate/nono.sha256"
pushgate_real_file "$HOME/.config/pushgate/nono-macos.json"
if pushgate_host_has shasum; then
PUSHGATE_SHA256="$(pushgate_host_tool shasum)" || exit 1
PUSHGATE_SHA256="$PUSHGATE_SHA256 -a 256"
else PUSHGATE_SHA256="$(pushgate_host_tool sha256sum)" || exit 1; fi
# A SYMLINK IS NOT A DIRECTORY YOU OWN.
# `mkdir -p` is satisfied by an existing symlink and FOLLOWS it, while the guard below only
# asks whether the literal path .pushgate is ignored and untracked. So an agent that had already
# written .pushgate/agent-home as a link to an unignored directory got the next launch to deposit
# its model-provider credentials there, where `git add -A` commits them. Every level is refused
# as a link first, and the created home is then proven to canonicalize back inside .pushgate.
pushgate_not_a_link() {
[ ! -L "$1" ] || {
echo "refusing: $1 is a symlink — the agent home must be real directories inside .pushgate, or credentials land wherever it points" >&2; return 1; }
}
pushgate_not_a_link .pushgate || exit 1
pushgate_not_a_link .pushgate/agent-home || exit 1
pushgate_not_a_link .pushgate/agent-home/.config || exit 1
pushgate_not_a_link .pushgate/agent-home/.cache || exit 1
pushgate_not_a_link .pushgate/agent-home/.codex || exit 1
pushgate_not_a_link .pushgate/agent-home/.claude || exit 1
mkdir -p .pushgate/agent-home/.config .pushgate/agent-home/.cache \
.pushgate/agent-home/.codex .pushgate/agent-home/.claude
__ah_real="$(cd -P .pushgate/agent-home 2>/dev/null && pwd -P)"
__pg_real="$(cd -P .pushgate 2>/dev/null && pwd -P)"
[ -n "$__ah_real" ] && [ -n "$__pg_real" ] || {
echo 'refusing: could not canonicalize the agent home, so it cannot be proven to sit inside .pushgate' >&2; exit 1; }
case "$__ah_real" in "$__pg_real"/*) ;; *)
echo "refusing: the agent home resolves outside .pushgate ($__ah_real) — credentials would be written where the ignore rule does not reach" >&2; exit 1 ;; esac
PUSHGATE_GIT="$(pushgate_host_tool git)" || exit 1
"$PUSHGATE_GIT" -c core.fsmonitor= -c core.hooksPath=/dev/null -c core.pager=cat check-ignore -q .pushgate || {
echo 'refusing: .pushgate is not git-ignored: add ".pushgate/" to .gitignore, or to .git/info/exclude to keep it out of your diff' >&2; exit 1; }
[ -z "$("$PUSHGATE_GIT" -c core.fsmonitor= -c core.hooksPath=/dev/null -c core.pager=cat ls-files -- .pushgate)" ] || {
echo 'refusing: .pushgate is tracked — run "git rm -r --cached .pushgate"' >&2; exit 1; }
PUSHGATE_NONO= PUSHGATE_NONO_DIGEST=
read -r PUSHGATE_NONO 2>/dev/null < "$HOME/.config/pushgate/nono.path" || true
[ -n "$PUSHGATE_NONO" ] || {
echo 'refusing: no attested nono on record — run the setup block above first' >&2; exit 1; }
pushgate_trusted_path "$PUSHGATE_NONO"
read -r PUSHGATE_NONO_DIGEST PUSHGATE_NONO_DIGEST_SUBJECT 2>/dev/null < "$HOME/.config/pushgate/nono.sha256" || true
[ -n "$PUSHGATE_NONO_DIGEST" ] || {
echo 'refusing: no recorded digest for the attested nono — run the setup block above first' >&2; exit 1; }
PUSHGATE_RUN="$(umask 077 && mktemp -d "$HOME/.config/pushgate/launch.XXXXXX")" && [ -n "$PUSHGATE_RUN" ] || {
echo 'refusing: could not create a private launch directory' >&2; exit 1; }
trap 'rm -rf "$PUSHGATE_RUN"' EXIT
pushgate_trusted_path "$PUSHGATE_RUN"
pushgate_real_dirs "$PUSHGATE_RUN"
cp "$PUSHGATE_NONO" "$PUSHGATE_RUN/nono" || {
echo 'refusing: could not make a private copy of nono' >&2; exit 1; }
pushgate_no_acl "$PUSHGATE_RUN/nono"
printf '%s %s\n' "$PUSHGATE_NONO_DIGEST" "$PUSHGATE_RUN/nono" | $PUSHGATE_SHA256 -c - >/dev/null 2>&1 || {
echo 'refusing: the attested nono has changed since it was verified' >&2; exit 1; }
cp "$HOME/.config/pushgate/nono-macos.json" "$PUSHGATE_RUN/nono-macos.json" || {
echo 'refusing: could not make a private copy of the nono profile' >&2; exit 1; }
pushgate_no_acl "$PUSHGATE_RUN/nono-macos.json"
printf '%s %s\n' 'af1c5d4c64f6e1b83adf5ca0b38761c8575bdc4bf4adf95bd8c27e4475de166e' "$PUSHGATE_RUN/nono-macos.json" | $PUSHGATE_SHA256 -c - >/dev/null 2>&1 || {
echo 'refusing: the installed nono profile is not the one this page publishes -- run the setup block above again' >&2; exit 1; }
PATH="$PUSHGATE_AMBIENT_PATH" "$PUSHGATE_RUN/nono" run --profile "$PUSHGATE_RUN/nono-macos.json" \
--workdir "$PWD" --allow-cwd -- codex --sandbox danger-full-access --ask-for-approval on-request
On macOS, copy the block, then run bash <(pbpaste) in your terminal. Pasting the block itself fails in the default macOS shells.
# ALPS 0.1 TRUSTED BOOTSTRAP — the first thing this block does, before any command runs.
# PATH is pinned to the base system, so every utility below (env, find, mkdir, mktemp, ...)
# is the host's own and never this repository's. The PATH you had is kept aside for two
# uses only: LOCATING the third-party tools this block then verifies, and handing a
# verified tool the PATH its own helpers expect. A second block pasted into the same
# shell keeps the PATH the first one saved, rather than saving the pin as if it were yours.
PUSHGATE_AMBIENT_PATH="${PUSHGATE_AMBIENT_PATH:-$PATH}"
export PATH='/usr/bin:/bin'
# SAME-UID LIMIT — what the checks below do, and what they cannot do.
# pushgate_host_tool refuses a tool supplied from inside this workspace, and refuses one whose
# file or parent directory is world-writable. That catches a repository-controlled PATH entry and
# a binary any OTHER local user could have replaced. It does NOT establish that the tool is
# genuine. Someone already running as YOUR user can write $HOME/bin/nono or $HOME/bin/gh with
# ordinary 0755 permissions: outside the workspace, not world-writable, so it passes every check
# here and is then invoked as the "verified" tool. A fake 'gh attestation verify' exits 0 and a
# fake 'nono' prints the expected version, and the agent is recorded as sandboxed and launched
# with no sandbox at all. This applies to every tool resolved below, cosign, curl and cilock
# included, and to the attestation check that verifies nono.
# No file-permission test can close this: the check runs with exactly the privileges of the
# process it would have to catch. The mitigation is a different privilege domain, not a stronger
# probe — run the agent under a SEPARATE UID, in a container, or in a VM, so that writing
# $HOME/bin is not something it can do in the first place.
#
# GROUP-WRITE, separately, IS yours to close. These checks test the other-write bit and not the
# group-write bit, and Homebrew's default prefix is group-writable: measured on Apple silicon,
# /opt/homebrew/bin is 'drwxrwxr-x <user>:admin' (Intel /usr/local/bin is the same). So any other
# member of that group can replace nono, gh, cosign or cilock before confinement starts and these
# checks still report host-owned. That is a different principal from you, unlike the case above.
# It is not rejected automatically because rejecting it refuses every Homebrew-installed tool,
# which is the install path this guide recommends. If anyone else has admin on this machine,
# close it yourself: chmod g-w /opt/homebrew/bin, or install these tools outside a group-writable
# prefix.
#
# OWNERSHIP ASSUMPTION, and its cost. A resolved tool is accepted only when YOU or ROOT own it,
# because any other owner can rewrite those bytes whenever they like. The cost lands on honest
# setups: a tool owned by a SERVICE ACCOUNT -- a CI runner toolchain under a build user, a
# linuxbrew prefix, a vendored toolchain installed as its own daemon user -- is refused here even
# though nobody is attacking. If that is your machine, it is a stated limit of this check rather than
# a finding: install or resolve the tool from a path owned by you or by root. It is deliberately
# not widened by guessing at a system-uid threshold, because macOS numbers its first human user
# 501 and Linux reserves under 1000 for daemons, so any single threshold would either trust a
# human peer on macOS or refuse a service account on Linux.
pushgate_host_has() { (PATH="$PUSHGATE_AMBIENT_PATH"; command -v "$1") >/dev/null 2>&1; }
pushgate_world_writable() {
__wwl="$(ls -ld "$1" 2>/dev/null)" || return 2
set -- $__wwl
[ $# -ge 3 ] || return 2
__wwo="$3"
__wwl="$1"
case "$__wwl" in ??????????*) ;; *) return 2 ;; esac
# WHO THIS PROCESS IS, resolved BEFORE any branch may answer "trusted".
# Every acceptance below is a statement about the owner, so an acceptance
# reached without this value is an acceptance made without asking the
# question. Recomputed on every call rather than cached across the walk:
# caching would let a __wwme already set in the pasted shell's environment
# decide who this process is, which hands the same-UID attacker a fresh lever
# to buy back a few forks the walk would never have noticed.
__wwme="$(id -un 2>/dev/null)" || return 2
[ -n "$__wwme" ] || return 2
# A STICKY directory is world-writable but restricted-deletion: anyone may
# create their own entries, and only the owner of an existing entry may
# unlink or rename it. That is precisely the question here — can someone
# else swap these bytes — so /tmp and friends are NOT replaceable. Without
# this, the ancestor walk below rejects every tool under /tmp on Linux,
# where TMPDIR is /tmp (drwxrwxrwt); macOS hides the bug because TMPDIR is a
# private per-user /var/folders path. CI caught exactly that difference.
#
# RESTRICTED-DELETION DOES NOT BIND THE DIRECTORY'S OWN OWNER. Sticky stops a
# STRANGER from unlinking somebody else's entry; the owner of the directory
# may still rename or remove anything inside it. So the sticky bit answers
# "can a stranger swap this" and says nothing about the owner, and a 1777
# directory belonging to another principal can replace the verified staging
# subtree between the integrity check and the bash that runs it. That is
# this file's own defect one branch further in — a mode bit accepted as an
# answer to a question about ownership — so the sticky branch asks the same
# question every other acceptance asks instead of returning early.
case "$__wwl" in d????????[tT]*)
case "$__wwo" in "$__wwme"|'root') return 1 ;; *) return 0 ;; esac ;;
esac
__wwl="${__wwl#????????}"
case "$__wwl" in w*) return 0 ;; -*) ;; *) return 2 ;; esac
# WHO OWNS IT, not just who else can write it. A 0755 file has its other-write
# bit clear and is still rewritable at will BY ITS OWNER, so a tool owned by
# another local user was being accepted as host-owned on the strength of a bit
# that says nothing about them. Same defect as checking a directory without its
# ancestors: the thing was validated and its context was not.
#
# Only this user and root qualify. Root because the base system is not
# installed by the reader; this user because that is the same-UID boundary
# stated below, which no file mode can close. Any other owner is a different
# principal who can rewrite these bytes whenever they like.
#
# THE FALSE POSITIVE THIS BUYS, NAMED RATHER THAN DISCOVERED. A tool owned by
# a SERVICE ACCOUNT -- a Linux runner's toolchain under a build user, a
# linuxbrew prefix, a vendored toolchain installed as its own daemon user --
# is refused here even though nobody is attacking. That is a real cost and it
# falls on honest configurations, which is the failure mode that gets a
# security control deleted rather than fixed, so it is stated in the block the
# reader pastes and the refusal below says what to do about it.
#
# It is not auto-widened, and the reason is that the obvious widening does not
# survive contact: 'trust uids below the system threshold' needs a threshold,
# and there is no single one. macOS numbers its first human user 501 while
# Linux reserves everything under 1000 for system accounts, so any constant
# either trusts a macOS human being or refuses a Linux service account. A
# heuristic that guesses wrong in the first direction is worse than a refusal
# a reader can read and act on.
case "$__wwo" in "$__wwme"|'root') return 1 ;; *) return 0 ;; esac
}
# Every directory from a path up to / — a writable ancestor lets a principal
# rename the directory below it and substitute the whole subtree, so checking
# only the immediate parent leaves the same replacement one level up.
pushgate_world_writable_path() {
__wwp="$1"
while : ; do
pushgate_world_writable "${__wwp:-/}"; __wwr=$?
[ "$__wwr" -eq 1 ] || return "$__wwr"
case "$__wwp" in ""|"/") return 1 ;; esac
__wwp="${__wwp%/*}"
done
}
pushgate_host_stage() {
__stage="$(mktemp -d)" || {
echo 'refusing: could not create a private staging directory' >&2; return 1; }
[ -n "$__stage" ] || {
echo 'refusing: empty staging directory path' >&2; return 1; }
__stage="$(cd -P "$__stage" 2>/dev/null && pwd -P)"
[ -n "$__stage" ] || {
echo 'refusing: could not canonicalize the staging directory, so it cannot be proven outside the workspace' >&2; return 1; }
__stage_pwd="$(cd -P "$PWD" 2>/dev/null && pwd -P)"
[ -n "$__stage_pwd" ] || {
echo 'refusing: cannot resolve the workspace directory, so nothing can be proven outside it' >&2; return 1; }
case "$__stage" in "$__stage_pwd"|"$__stage_pwd"/*)
echo "refusing: TMPDIR stages inside the workspace ($__stage) — verified bytes would sit where the agent this boundary contains can rewrite them" >&2; return 1 ;; esac
# THE WHOLE CHAIN, not the leaf. mktemp -d makes a 0700 directory, and that
# says nothing about the directories above it: a non-sticky world-writable
# ancestor lets another user rename this one away and substitute their own
# between the integrity check and the bash that runs the result. The
# ancestor walk is the same one the resolved tool paths get.
pushgate_world_writable_path "$__stage"; __stage_ww=$?
[ "$__stage_ww" -eq 1 ] || {
echo "refusing: the staging directory, or a directory above it, is world-writable, owned by another principal, or could not be read" >&2; return 1; }
printf '%s\n' "$__stage"
}
pushgate_host_tool() {
__invoke="$(PATH="$PUSHGATE_AMBIENT_PATH"; command -v "$1" 2>/dev/null || true)"
case "$__invoke" in /*) ;; *)
echo "refusing: no host-owned $1 on PATH" >&2; return 1 ;; esac
__tool="$__invoke"
__hops=0
while [ -L "$__tool" ] && [ "$__hops" -lt 16 ]; do
__link="$(readlink "$__tool")"
case "$__link" in
/*) __tool="$__link" ;;
*) __tool="${__tool%/*}/$__link" ;;
esac
__hops=$((__hops + 1))
done
__dir="${__tool%/*}"
__tool="$(cd -P "${__dir:-/}" 2>/dev/null && pwd -P)/${__tool##*/}"
__dir="${__invoke%/*}"
__invoke="$(cd -P "${__dir:-/}" 2>/dev/null && pwd -P)/${__invoke##*/}"
__invoke_dir="${__invoke%/*}"
__pwd="$(cd -P "$PWD" 2>/dev/null && pwd -P)"
[ -n "$__pwd" ] || {
echo "refusing: cannot resolve the workspace directory, so nothing can be excluded from it" >&2; return 1; }
case "$__tool" in "$__pwd"/*)
echo "refusing: $1 resolves inside the workspace ($__tool) — a repository-controlled PATH entry cannot supply it" >&2; return 1 ;; esac
case "$__invoke" in "$__pwd"/*)
echo "refusing: $1 is supplied from inside the workspace ($__invoke) — a repository-controlled PATH entry cannot supply it" >&2; return 1 ;; esac
[ -x "$__tool" ] || { echo "refusing: $__tool is not executable" >&2; return 1; }
pushgate_world_writable "$__tool"; __ww=$?
[ "$__ww" -eq 1 ] || {
echo "refusing: $__tool is world-writable, owned by a user other than you or root, or its permissions could not be read, so it is not provably host-owned. If it is owned by a service account you trust -- a runner toolchain, a linuxbrew prefix -- that is a stated limitation of this check, not an attack: install or resolve the tool from a path owned by you or by root." >&2; return 1; }
# THE RESOLVED TARGET'S OWN DIRECTORY CHAIN, not just the invocation's.
# A symlink on a tight path pointing into a writable directory was accepted:
# the link and its directory both looked fine while the bytes it named could
# be replaced at will. The file's mode says nothing about that — replacing a
# file needs write on its DIRECTORY, so the directory is the thing to check.
pushgate_world_writable_path "${__tool%/*}"; __ww=$?
[ "$__ww" -eq 1 ] || {
echo "refusing: the directory holding $__tool is world-writable, or its permissions could not be read, so $1 could be replaced there" >&2; return 1; }
pushgate_world_writable_path "${__invoke_dir:-/}"; __ww=$?
[ "$__ww" -eq 1 ] || {
echo "refusing: ${__invoke_dir:-/} is world-writable, or its permissions could not be read, so $1 could be repointed" >&2; return 1; }
printf '%s\n' "$__invoke"
}
__pg_tw="$(pwd -P)" && [ -n "$__pg_tw" ] || {
echo 'refusing: could not resolve the workspace directory' >&2; exit 1; }
__pg_tm="${TMPDIR:-/tmp}"
__pg_tp="$(cd -P "$__pg_tm" 2>/dev/null && pwd -P)" || __pg_tp="$__pg_tm"
pushgate_agent_writable() {
case "$1/" in "${__pg_tw%/}"/*|"${__pg_tm%/}"/*|"${__pg_tp%/}"/*) return 0 ;; esac
__pg_n="${1#/private}"
case "$1/" in /private/*) ;; *) __pg_n="$1" ;; esac
case "$__pg_n/" in /tmp/*|/var/folders/*|/dev/*) return 0 ;; esac
return 1
}
__pg_nl='
'
__pg_r1="$HOME/.config/pushgate" __pg_r2="$HOME/.local/share/pushgate"
pushgate_no_acl() {
case "$1/" in "${__pg_r1%/}"/*|"${__pg_r2%/}"/*) __pg_am=any ;; *) __pg_am=allow ;; esac
if __pg_al="$(ls -lde "$1" 2>/dev/null)"; then
case "$__pg_al" in *"$__pg_nl"*) __pg_al="${__pg_al#*"$__pg_nl"}" ;; *) return 0 ;; esac
if [ "$__pg_am" = allow ]; then
case "$__pg_al" in *" allow"*) ;; *) return 0 ;; esac
fi
elif __pg_al="$(ls -ld "$1" 2>/dev/null)"; then
case "${__pg_al%% *}" in ??????????+*) ;; *) return 0 ;; esac
fi
echo "refusing: $1 carries an access control list -- or its ACL could not be read -- so another account could be granted write to what this boundary trusts" >&2; exit 1
}
pushgate_trusted_path() {
case "$1" in /*) ;; *) echo "refusing: $1 is not an absolute path" >&2; exit 1 ;; esac
__pg_left="${1#/}" __pg_at= __pg_hops=0
while [ -n "$__pg_left" ]; do
__pg_c="${__pg_left%%/*}"
case "$__pg_left" in */*) __pg_left="${__pg_left#*/}" ;; *) __pg_left= ;; esac
[ -n "$__pg_c" ] && [ "$__pg_c" != . ] || continue
[ "$__pg_c" != .. ] || { __pg_at="${__pg_at%/*}"; continue; }
__pg_at="$__pg_at/$__pg_c"
! pushgate_agent_writable "$__pg_at" || {
echo "refusing: $1 passes through $__pg_at, which is inside the workspace or inside a directory a sandboxed agent can write, so the agent could redirect it" >&2; exit 1; }
if [ -e "$__pg_at" ] || [ -L "$__pg_at" ]; then pushgate_no_acl "$__pg_at"; fi
[ -L "$__pg_at" ] || continue
__pg_hops=$((__pg_hops + 1))
__pg_l="$(readlink "$__pg_at")" && [ -n "$__pg_l" ] && [ "$__pg_hops" -le 32 ] || {
echo "refusing: could not resolve the link $__pg_at in $1" >&2; exit 1; }
case "$__pg_l" in /*) __pg_at= ;; *) __pg_at="${__pg_at%/*}" ;; esac
__pg_left="${__pg_l#/}${__pg_left:+/$__pg_left}"
done
case "$__pg_tw/" in "${__pg_at%/}"/*)
echo "refusing: the workspace $__pg_tw is inside $__pg_at ($1), which this boundary trusts" >&2; exit 1 ;; esac
}
pushgate_real_dirs() {
__pg_left="${1#/}" __pg_at=
while [ -n "$__pg_left" ]; do
__pg_c="${__pg_left%%/*}"
case "$__pg_left" in */*) __pg_left="${__pg_left#*/}" ;; *) __pg_left= ;; esac
[ -n "$__pg_c" ] || continue
__pg_at="$__pg_at/$__pg_c"
# Not there yet is not a link; whatever reads it next refuses on its own.
[ -e "$__pg_at" ] || [ -L "$__pg_at" ] || return 0
[ ! -L "$__pg_at" ] && [ -d "$__pg_at" ] || {
echo "refusing: $__pg_at is a symlink or not a directory, and $1 must sit on real directories no sandbox can redirect -- run the setup block above again" >&2; exit 1; }
pushgate_no_acl "$__pg_at"
case "$__pg_at/" in "${HOME%/}"/*) [ -O "$__pg_at" ] || {
echo "refusing: $__pg_at is not owned by you, so another account could rewrite $1" >&2; exit 1; } ;; esac
done
}
pushgate_real_file() {
__pg_f="$1"
pushgate_real_dirs "${__pg_f%/*}"
[ -e "$1" ] || [ -L "$1" ] || return 0
[ -f "$1" ] && [ ! -L "$1" ] || {
echo "refusing: $1 is a symlink or not a regular file -- run the setup block above again" >&2; exit 1; }
pushgate_no_acl "$1"
}
pushgate_trusted_path "$HOME/.config/pushgate"
pushgate_trusted_path "$HOME/.local/share/pushgate"
pushgate_real_dirs "$HOME/.config/pushgate"
pushgate_trusted_path "$HOME/.config/pushgate/srt-macos.json"
pushgate_trusted_path "$HOME/.local/share/pushgate/srt-0.0.73"
pushgate_trusted_path "$HOME/.local/share/pushgate/srt-0.0.73/node_modules/.bin/srt"
pushgate_real_dirs "$HOME/.local/share/pushgate/srt-0.0.73"
__pg_sb=
read -r __pg_sb < "$HOME/.local/share/pushgate/srt-0.0.73/node_modules/.bin/srt" || true
case "$__pg_sb" in '#!'*)
set -- ${__pg_sb#??}
__pg_ip="$1" __pg_ia="$2"
pushgate_trusted_path "$__pg_ip"
if [ "${__pg_ip##*/}" = env ]; then
__pg_ip="$(PATH="/opt/homebrew/opt/node@22/bin:/opt/homebrew/bin:/usr/local/opt/node@22/bin:/usr/local/bin:/usr/bin:/bin" command -v "$__pg_ia")" && [ -n "$__pg_ip" ] || {
echo "refusing: the Sandbox Runtime interpreter $__pg_ia is not on the launch PATH" >&2; exit 1; }
pushgate_trusted_path "$__pg_ip"
fi ;;
esac
__pg_sr="$(cd -P "$HOME/.local/share/pushgate/srt-0.0.73" 2>/dev/null && pwd -P)" && [ -n "$__pg_sr" ] || {
echo 'refusing: no Sandbox Runtime at $HOME/.local/share/pushgate/srt-0.0.73 -- run the setup block above first' >&2; exit 1; }
__pg_bad="$(find "$__pg_sr" ! -type f ! -type d ! -type l -o ! -user "$(id -u)" -o -type f -links +1 \
-o ! -type l -perm -0020 -o ! -type l -perm -0002 -o -name "*$__pg_nl*")" && [ -z "$__pg_bad" ] || {
echo "refusing: the Sandbox Runtime tree may hold only your own regular files, directories and links, with no hard links and nothing another account can write -- these are not: $__pg_bad" >&2; exit 1; }
if ls -lde "$__pg_sr" >/dev/null 2>&1; then
__pg_ls="$(ls -lRAe "$__pg_sr")" && __pg_re='^ *[0-9][0-9]*: '
else
__pg_ls="$(ls -lRA "$__pg_sr")" && __pg_re='^[^ ][^ ][^ ][^ ][^ ][^ ][^ ][^ ][^ ][^ ][+]'
fi || {
echo 'refusing: could not list the Sandbox Runtime tree to read its access control lists' >&2; exit 1; }
printf '%s\n' "$__pg_ls" | grep -Eq "$__pg_re"
[ $? -eq 1 ] || {
echo "refusing: an entry in the Sandbox Runtime tree carries an access control list -- or its ACLs could not be read -- so another account could be granted write to it" >&2; exit 1; }
pushgate_tree_link() {
__pg_left="$1" __pg_at= __pg_hops=0
while [ -n "$__pg_left" ]; do
__pg_c="${__pg_left%%/*}"
case "$__pg_left" in */*) __pg_left="${__pg_left#*/}" ;; *) __pg_left= ;; esac
[ -n "$__pg_c" ] && [ "$__pg_c" != . ] || continue
if [ "$__pg_c" = .. ]; then
[ -n "$__pg_at" ] || {
echo "refusing: the link $__pg_sr/$1 climbs out of the Sandbox Runtime tree, where another sandbox could rewrite what it reaches" >&2; exit 1; }
__pg_at="${__pg_at%/*}"; continue
fi
__pg_at="$__pg_at/$__pg_c"
[ -L "$__pg_sr$__pg_at" ] || continue
__pg_hops=$((__pg_hops + 1))
__pg_l="$(readlink "$__pg_sr$__pg_at")" && [ -n "$__pg_l" ] && [ "$__pg_hops" -le 40 ] || {
echo "refusing: could not resolve the link $__pg_sr/$1 inside the Sandbox Runtime tree -- it loops or takes more than 40 hops" >&2; exit 1; }
case "$__pg_l" in /*)
echo "refusing: the link $__pg_sr$__pg_at in the Sandbox Runtime tree is absolute ($__pg_l) -- every link in it must be relative and stay inside the tree" >&2; exit 1 ;; esac
__pg_at="${__pg_at%/*}"
__pg_left="$__pg_l${__pg_left:+/$__pg_left}"
done
}
__pg_links="$(find "$__pg_sr" -type l)" || {
echo 'refusing: could not list the links in the Sandbox Runtime tree' >&2; exit 1; }
printf '%s\n' "$__pg_links" | while IFS= read -r __pg_ln; do
[ -z "$__pg_ln" ] || pushgate_tree_link "${__pg_ln#"$__pg_sr"/}"
done || exit 1
pushgate_real_file "$HOME/.config/pushgate/srt-macos.json"
if pushgate_host_has shasum; then
PUSHGATE_SHA256="$(pushgate_host_tool shasum)" || exit 1
PUSHGATE_SHA256="$PUSHGATE_SHA256 -a 256"
else PUSHGATE_SHA256="$(pushgate_host_tool sha256sum)" || exit 1; fi
# A SYMLINK IS NOT A DIRECTORY YOU OWN.
# `mkdir -p` is satisfied by an existing symlink and FOLLOWS it, while the guard below only
# asks whether the literal path .pushgate is ignored and untracked. So an agent that had already
# written .pushgate/agent-home as a link to an unignored directory got the next launch to deposit
# its model-provider credentials there, where `git add -A` commits them. Every level is refused
# as a link first, and the created home is then proven to canonicalize back inside .pushgate.
pushgate_not_a_link() {
[ ! -L "$1" ] || {
echo "refusing: $1 is a symlink — the agent home must be real directories inside .pushgate, or credentials land wherever it points" >&2; return 1; }
}
pushgate_not_a_link .pushgate || exit 1
pushgate_not_a_link .pushgate/agent-home || exit 1
pushgate_not_a_link .pushgate/agent-home/.config || exit 1
pushgate_not_a_link .pushgate/agent-home/.cache || exit 1
pushgate_not_a_link .pushgate/agent-home/.codex || exit 1
pushgate_not_a_link .pushgate/agent-home/.claude || exit 1
mkdir -p .pushgate/agent-home/.config .pushgate/agent-home/.cache \
.pushgate/agent-home/.codex .pushgate/agent-home/.claude
__ah_real="$(cd -P .pushgate/agent-home 2>/dev/null && pwd -P)"
__pg_real="$(cd -P .pushgate 2>/dev/null && pwd -P)"
[ -n "$__ah_real" ] && [ -n "$__pg_real" ] || {
echo 'refusing: could not canonicalize the agent home, so it cannot be proven to sit inside .pushgate' >&2; exit 1; }
case "$__ah_real" in "$__pg_real"/*) ;; *)
echo "refusing: the agent home resolves outside .pushgate ($__ah_real) — credentials would be written where the ignore rule does not reach" >&2; exit 1 ;; esac
PUSHGATE_GIT="$(pushgate_host_tool git)" || exit 1
"$PUSHGATE_GIT" -c core.fsmonitor= -c core.hooksPath=/dev/null -c core.pager=cat check-ignore -q .pushgate || {
echo 'refusing: .pushgate is not git-ignored: add ".pushgate/" to .gitignore, or to .git/info/exclude to keep it out of your diff' >&2; exit 1; }
[ -z "$("$PUSHGATE_GIT" -c core.fsmonitor= -c core.hooksPath=/dev/null -c core.pager=cat ls-files -- .pushgate)" ] || {
echo 'refusing: .pushgate is tracked — run "git rm -r --cached .pushgate"' >&2; exit 1; }
PUSHGATE_RUN="$(umask 077 && mktemp -d "$HOME/.config/pushgate/launch.XXXXXX")" && [ -n "$PUSHGATE_RUN" ] || {
echo 'refusing: could not create a private launch directory' >&2; exit 1; }
trap 'rm -rf "$PUSHGATE_RUN"' EXIT
pushgate_trusted_path "$PUSHGATE_RUN"
pushgate_real_dirs "$PUSHGATE_RUN"
cp "$HOME/.config/pushgate/srt-macos.json" "$PUSHGATE_RUN/srt-macos.json" || {
echo 'refusing: could not make a private copy of the Sandbox Runtime settings' >&2; exit 1; }
pushgate_no_acl "$PUSHGATE_RUN/srt-macos.json"
printf '%s %s\n' 'a3ba8b7beae510d7e0b01e3c8f31354e374bea173cb8981c5ba8df329b9f710f' "$PUSHGATE_RUN/srt-macos.json" | $PUSHGATE_SHA256 -c - >/dev/null 2>&1 || {
echo 'refusing: the installed Sandbox Runtime settings is not the one this page publishes -- run the setup block above again' >&2; exit 1; }
env -i HOME="$PWD/.pushgate/agent-home" \
XDG_CONFIG_HOME="$PWD/.pushgate/agent-home/.config" \
CODEX_HOME="$PWD/.pushgate/agent-home/.codex" \
CLAUDE_CONFIG_DIR="$PWD/.pushgate/agent-home/.claude" \
PATH="/opt/homebrew/opt/node@22/bin:/opt/homebrew/bin:/usr/local/opt/node@22/bin:/usr/local/bin:/usr/bin:/bin" \
TERM="${TERM:-xterm-256color}" \
"$HOME/.local/share/pushgate/srt-0.0.73/node_modules/.bin/srt" \
--settings "$PUSHGATE_RUN/srt-macos.json" -- codex --sandbox danger-full-access --ask-for-approval on-request
On macOS, copy the block, then run bash <(pbpaste) in your terminal. Pasting the block itself fails in the default macOS shells.
Model-client authentication stays inside the synthetic home and receives no TestifySec, Git-signing, or infrastructure authority.
Evidence a verifier checks
ALPS 1 facts plus measured agent and CI/lock executable digests, the sandbox-policy digest actually loaded, process ancestry, exec-time environment allowlist, mount and rootfs state, workspace/toolchain/cache digests, filesystem/network policy, and observer-covered egress and denials. Negative proof is limited to the observer’s stated coverage.
Benefits and mitigations
Mitigates prompt-injection attempts to read or exfiltrate signing material, generic signer-socket access, PATH or binary substitution, direct credential use, and unapproved egress.
Limits
The sandbox, kernel, runtime, allowlist, exact CI/lock binary, attestors, and test program remain trusted. A sandbox escape, host administrator, compromised allowed tool, dishonest test, or policy mistake is out of scope. Evidence proves observed execution and output—not semantic correctness.
Derive hermeticity separately
Mermaid source
flowchart LR
O[Trusted observer evidence] --> D{Derive separately from ALPS 0.1}
D --> A[H-Open: external influence permitted and recorded]
D --> C[H-Constrained: allowlist enforced and inputs observed]
D --> H[H-Complete: no unmeasured influence crosses named boundary]
M[Missing or insufficient coverage] --> U[Hermeticity: Unknown]ALPS 0.1 measures provenance and isolation between an agent and signing authority. Hermeticity measures external runtime influence. A verifier derives the two results independently and can report a compact pair such as ALPS-2 / H-Constrained.
Evidence a verifier checks
- H-Open: signed observations identify the external runtime influence that was permitted and record relevant destinations and inputs.
- H-Constrained: a trusted boundary enforced an explicit allowlist—including named public or private API providers—and observed allowed destinations and relevant external inputs.
- H-Complete: all material inputs were staged and content-identified; network was disabled or confined entirely within the independently measured boundary; and a trusted observer verified that no unmeasured external influence crossed it.
Limits
The evidence must name the assessed boundary and its scope. Ambient environment, file descriptors, sockets, filesystem roots, toolchain, caches, time, randomness, and host inputs must be bounded or explicit within that coverage.
Where the model runs decides how far the boundary reaches. An on-premises customer-operated inference cluster, or an independently attested dedicated cloud inference environment, can be inside an expanded closed boundary. A managed model endpoint reached through standard private connectivity such as AWS PrivateLink remains H-Constrained when the provider’s model or runtime influence is not fully measured.
“No observed network” alone is not proof of hermeticity. Missing or insufficient evidence produces Hermeticity: Unknown, not H-Open.
ALPS 3 · Isolated
No environment reaches ALPS 3 today. It needs the cilockd signing service outside the agent, on Linux, macOS or Windows, and cilockd has not shipped (support matrix). When it ships, this tier packages its administration and protected signing services. The ALPS level name remains vendor-neutral.
Mermaid source
flowchart LR
A[Agent sandbox] -->|typed proof request| D[CI/lock service]
D -->|validate repo + commit + command + output| H[Non-exportable hardware identity]
H -->|keyless leaf + RFC 3161| P[TestifySec platform]
P --> E[Signed evidence]
G[No generic signing oracle] -.-> DSummary
A separate service independently re-derives each typed proof request before protected signing authority can authorize it.
Intended for
Organizations that treat the coding-agent process as potentially fully compromised or adversarial.
Requirements
The same CI/lock distribution has two roles: an unprivileged client in the agent sandbox and a separate service or control plane outside it. The service accepts only typed proof requests and independently re-derives repository, commit, command, predicate, output digest, expiry, and replay context. It never exposes a sign-arbitrary-bytes endpoint.
The service bootstrap identity is non-exportable: TPM 2.0, an HSM, Apple Secure Enclave on Apple silicon, the T2 Security Chip on older Intel Macs, or a custom KMS solution.
macOS deployments verify Developer ID, notarization, and a designated requirement. Linux deployments verify signed packages or container digests, and can add fs-verity/IMA plus TPM measured-boot quotes.
Hardware usually protects the daemon, device, or workload bootstrap identity. Fulcio can still mint an ephemeral leaf for each evidence signature, and every signature remains RFC 3161 timestamped.
Evidence a verifier checks
ALPS 2 facts plus typed-request re-derivation, measured and signed CI/lock, isolated service identity, hardware or remote-key attestation, and evidence that signing authority was unavailable to the agent process.
Benefits and mitigations
Mitigates full agent-process compromise, agent-side key extraction, arbitrary-bytes signing, local binary substitution, and direct access to signing authority.
Limits
The service and platform implementations, hardware root and administrator controls, policy, verifier roots, TSA, and the tools whose outputs are attested remain trusted. This level does not prove code is bug-free or tests are sufficient.
Verify the boundary before onboarding
Run these controls with disposable fixtures. Every denied signing attempt must leave HEAD and repository refs unchanged.
- Read a synthetic key outside the workspace directly and through a workspace symlink: both must fail at ALPS 2.
- Try inherited and guessed SSH-agent, Docker, and signer sockets: they must be absent, unmounted, and unreachable.
- Try a fake
cilock, PATH shadow, direct executable path, and writable replacement: none may inherit the pinned command policy. - Run CI/lock twice: both approved invocations work, and each receives a fresh child-tool policy.
- Attempt direct platform access from the agent: the egress and credential policy must deny it.
- Produce proof through the approved path and verify its observer facts, exact subjects, Fulcio identity, and RFC 3161 timestamp.
Scope notes
Pin Sandbox Runtime 0.0.73 and retest it before upgrades. Linux pathname sockets must be absent from the child namespace, not merely removed from its environment. These containment fixtures do not treat macOS Keychain, Secure Enclave, or T2 custody as evidence of signer isolation; ALPS 3 requires independently verified non-exportability evidence.
Reference generated from the product documentation. Match commands and support details to your installed release.