This is the operator guide to the local-agent sandbox: launching a CLI coding
agent (Claude Code, Codex, Cursor's cursor-agent, Hermes) as its own
unprivileged Unix user, so a compromised or prompt-injected agent cannot read
your SSH keys, browser session, cloud credentials, or the Jentic One credential
store.
It covers the flow and day-to-day usage with examples. For the security design
and rationale, see docs/security/same-host/;
for the generated per-flag reference, open /app/docs on your deployment
(Reference → CLI).
- The mental model
- Quickstart
- What
jentic setupsets up - The
jentic runflow - Cookbook
- Where state lives
- Tearing it all down —
jentic reset - Troubleshooting
- Platform support
Three mechanisms stack, and all three must hold for a session to start:
flowchart TB
subgraph L1["1 · Identity boundary (permanent)"]
A["Dedicated Unix user<br/><i><you>-local-agent</i><br/>home: /Users/Shared/… or /opt/…"]
end
subgraph L2["2 · Directory grants (durable ACLs)"]
B["Execute-only 'traverse' ACEs down the path<br/>+ recursive read/write ACE on each granted workspace"]
end
subgraph L3["3 · Per-session sandbox (every launch)"]
C["sandbox-exec (macOS) / bwrap (Linux)<br/>denies ALL of /Users and /home,<br/>re-opens only agent home + grants,<br/>tool dirs mounted read-only"]
end
L1 --> L2 --> L3
The result, from the agent's point of view:
| Path | Access | Why |
|---|---|---|
Agent's own home (/Users/Shared/<agent> / /opt/<agent>) |
✅ read/write | Its workspace-of-last-resort; you keep an inherited read/write ACL into it |
| Each directory you granted | ✅ read/write | Recursive inherited ACL + sandbox re-open |
The agent's own ~/.local/bin |
✅ read + execute, ❌ write | So it can't rewrite its own launched binary |
System / Homebrew tool dirs (/usr/bin, /opt/homebrew/bin, …) |
✅ read + execute, ❌ write | Shared tools without letting the agent replace them |
| Your home — everything not granted | ❌ | Sandbox denies /Users + /home wholesale |
~/.ssh, ~/.aws, ~/.jentic, keychains, browser profiles |
❌ never grantable | Hard-banned: the grant prompt offers no "grant anyway" |
System trees (/etc, /usr, /var, /) |
❌ not grantable | Hard-banned |
Two honest limits (details in the security docs): the boundary protects you from the agent — it does not partition the agent from itself (anything seeded into the agent's home is readable by it), and it does not restrict network egress.
# 1. One-time: onboard. After you pick your agent, setup offers to isolate
# it behind a dedicated Unix user (asks before any sudo runs).
jentic setup
# 2. Every session: launch the agent, confined, in the project you name.
jentic run claude ~/projects/my-app
# 3. Done with the whole setup? Tear it down (prompts before anything destructive).
jentic resetA first jentic run in a directory the agent can't reach looks like this:
$ cd ~/projects/my-app && jentic run claude
⚠ Agent alice-local-agent has no access to /Users/alice/projects/my-app.
How should the session reach this directory?
Open in the agent's home instead
> Allow the agent read/write here (adds an inherited ACL)
Cancel
Granted (persists across sessions; `jentic run claude --list-grants` to review).
This is OS-level access only — the agent still runs its own workspace-trust prompt.
Launching claude as alice-local-agent in /Users/alice/projects/my-app (confined) ...
From then on that directory opens without a prompt.
No isolated account?
jentic runstill works: it launches the agent directly as you (no sandbox) and prints a one-time warning telling you so. Runjentic setupto get the isolated setup.
jentic setup takes a fresh machine to ready: register an agent identity
with your Jentic install, write the CLI-usage skill into your agent runtime, and
— the part this guide is about — offer to isolate the agent. The isolation step
is sudo-last: it asks "Create a dedicated user account for your local
agent? (requires sudo)" before any privileged command runs, so declining never
triggers a password prompt.
Opting in shows one prefilled, editable form:
| Field | Default | Notes |
|---|---|---|
| Agent account name | <you>-local-agent |
Validated as a safe Unix username |
| Agent home directory | /Users/Shared/<name> (macOS), /opt/<name> (Linux) |
Always outside every human home |
| Copy your agent config? | Yes when found | Names the exact files ("Will copy: ~/.claude, ~/.claude.json") |
| Copy your provider config? | Yes when detected | AWS Bedrock → ~/.aws; Google Vertex → ~/.config/gcloud |
| Passwordless launch? | Yes | Scoped sudoers rule to become the agent user — never root |
Then, if your agent has a record of trusted workspaces (Claude Code's
~/.claude.json projects you accepted the trust dialog for), setup offers
them as a pre-selected multiselect — "Bring your workspaces over" — so you
don't re-grant each project by hand on first run. Banned paths (see
the grant model) are never offered.
Non-interactive setup:
jentic setup --url https://jentic.example.com --operator claude --yes
jentic setup --skip-skill # identity only
jentic setup --dry-run # describe everything, change nothingPrerequisites are checked before the first sudo. If something is missing
(e.g. bwrap on a minimal Linux), setup prints the exact install command
for your package manager and offers to continue same-user — a missing dependency
never blocks the identity/skill provisioning you came for.
jentic run <agent> [path] [flags] [-- <agent-args>...]
jentic run -- <agent> [agent-args...]
<agent> picks the binary (claude, codex, cursor, hermes) — never
an account or identity. There is one shared agent account; identity comes from
your active context, exported into the agent's home at every launch.
What a launch does, in order:
flowchart TD
S(["jentic run claude ~/proj"]) --> ACC{"Isolated account<br/>recorded in config?"}
ACC -- no --> SAME["Run same-user as you<br/>(one-time warning, no sandbox)"]
ACC -- yes --> SUDO["Preflight: sudo -u agent true<br/>(the ONE place a password prompt appears)"]
SUDO --> BIN{"Agent binary installed<br/>for the agent user?"}
BIN -- missing --> PROV["Offer: copy your binary /<br/>run its installer as the agent / skip"]
BIN -- "on PATH" --> SEED
PROV --> SEED["Offer once: seed your agent config<br/>+ provider config (secrets scrubbed)"]
SEED --> DIR{"Can the agent already<br/>read/write the working dir?"}
DIR -- yes --> CTX
DIR -- no --> CLASS{"Path classification"}
CLASS -- banned --> HOMEOPT["Only: open in agent's home / cancel"]
CLASS -- ok --> GRANT["Prompt: Allow (ACL grant, persists) /<br/>open in home / cancel"]
GRANT --> CTX["Export your ACTIVE context into<br/>the agent's own config store (agent mode)"]
HOMEOPT --> CTX
CTX --> CONF{"Sandbox available?<br/>(sandbox-exec / bwrap)"}
CONF -- no --> REFUSE(["REFUSE — error-closed,<br/>prints install hints"])
CONF -- yes --> GO(["Confined session:<br/>sudo → sandbox → login shell → agent"])
Details worth knowing:
- One password prompt. All privileged steps go through the same
sudo -u <agent>; the preflight is where you're asked (or nothing, with the passwordless rule). A declined password is reported as a sudo problem, never misdiagnosed as "agent not installed". - Binary provisioning. Single-file agents (claude, codex) default to
copying your binary into the agent's
~/.local/bin; installer-based agents (cursor, hermes) run their documented installer as the agent user. Copying carries the binary, never your credentials — the agent authenticates its own tools on first launch. - Config seeding is opt-in, once, and never clobbers. It only runs while
the agent has no config of its own. Discrete credential files inside seeded
trees (Codex's
~/.codex/auth.json, Hermes's~/.hermes/.env) are scrubbed after the copy: the agent inherits your settings but authenticates as itself. - Confinement is required. If the machine can't sandbox the session,
jentic runrefuses (CONFINEMENT_UNAVAILABLE) rather than silently running unconfined — and prints the per-dependency fix. - Your environment doesn't leak. The launch forwards only terminal/locale
variables (
TERM,LANG, …) and explicitly unsets SSH/GPG agent handles, so the agent can't ride your forwarded ssh-agent.
jentic run claude # current directory (prompts to grant if needed)
jentic run claude ~/projects/api # explicit working directory
jentic run claude --home # start in the agent's own home — nothing to grant
jentic run codex ~/work/site # any runnable agent: claude, codex, cursor, hermesEverything after -- goes to the agent binary verbatim (like
cargo run -- / kubectl exec --). Two forms:
# Trailing --: jentic's own args (agent, optional path) come first.
jentic run claude -- --model opus -p "review this" # runs: claude --model opus -p "review this"
jentic run claude ~/work/api -- --resume # forwards --resume, workdir ~/work/api
# Leading --: the whole agent command follows the separator. Nothing after it is
# parsed by jentic, so agent flags can never collide with jentic flags.
jentic run -- claude --resumeSessionId=1234The leading form takes no path argument (the working directory is the current one) — use the trailing form when you need to pass a path. Forwarded arguments run inside the same confined session; they never widen the sandbox.
Grants are persistent (they're filesystem ACLs, not per-session), so review them occasionally:
jentic run claude --list-grants # what can the agent reach?
jentic run claude --grant ~/work/api # grant without launching
jentic run claude --revoke ~/work/api # revoke (leaf access drops immediately)$ jentic run claude --list-grants
Directory grants
agent: claude
user: alice-local-agent
dir: /Users/alice/projects/my-app
dir: /Users/alice/work/api
To take a directory away: `jentic run <agent> --revoke <dir>` (`--list-grants` to review).
What you can and can't grant:
| You ask for | Verdict | What happens |
|---|---|---|
~/projects/my-app |
✅ fine | Allow / open-in-home / cancel prompt |
~ (your home) |
⚠ soft ban | Not grantable itself — grant a subdirectory instead |
~/.config |
⚠ soft ban | Holds credential children; grant e.g. ~/.config/nvim instead |
/Users/bob |
⚠ soft ban | Another human's home root |
~/.ssh, ~/.aws, ~/.jentic, ~/.kube, browser profiles, keychains |
⛔ hard ban | Path and every descendant refused — no "grant anyway" exists |
/etc, /usr, /var, / |
⛔ hard ban | System trees |
Symlinks are resolved before classification, so a link into a banned tree is
caught. On macOS the check is case-insensitive (~/.SSH is still ~/.ssh).
jentic run claude --yes # take the safe default at every prompt
# (never grants a flagged-dangerous dir)
jentic run claude ~/proj --allow-dir # pre-answer the grant prompt with "Allow"
jentic run claude --no-allow-dir # never grant; open in the agent's home
jentic run claude --seed-config # copy your agent config without prompting
jentic run claude --no-seed-config # never copy it--allow-dir still hard-refuses a banned directory — flags can't override the
boundary.
| Agent | Binary | Provisioning default | Seeded config | Secrets scrubbed after seeding |
|---|---|---|---|---|
claude |
claude |
copy your binary | ~/.claude, ~/.claude.json |
— (key is embedded in the config Claude needs) |
codex |
codex |
copy your binary | ~/.codex |
~/.codex/auth.json |
cursor |
cursor-agent |
run its installer | ~/.cursor |
— |
hermes |
hermes |
run its installer | ~/.hermes |
~/.hermes/.env |
cursor here is the headless cursor-agent CLI — the Cursor GUI cannot run
under a separate Unix account (see
GUI IDEs
for the Remote-SSH pattern). generic is a skill-only target for
jentic skill; jentic run generic is refused with a pointer.
If your Claude Code setup points at a cloud provider, jentic run detects it
from ~/.claude/settings.json and offers to seed that provider's config:
settings.json env |
Provider | Seeded |
|---|---|---|
CLAUDE_CODE_USE_BEDROCK=1 |
AWS Bedrock | ~/.aws (config + profiles; not the cached SSO token) |
CLAUDE_CODE_USE_VERTEX=1 |
Google Vertex | ~/.config/gcloud + any GOOGLE_APPLICATION_CREDENTIALS file |
| neither | Anthropic API | nothing extra |
Both seed prompts carry the same warning: seeded credentials are readable by the agent. Prefer fronting the provider with an LLM proxy (e.g. LiteLLM) and a revocable virtual key, so nothing sensitive comes to rest in the agent account.
- The agent's
~/.local/binis prepended to its PATH (where its binary lands). - Your world-readable tool dirs (Homebrew's
/opt/homebrew/bin,/usr/local/bin,/snap/bin) are appended to the agent's PATH sogit,jq,rg,nodeetc. just work — read-only inside the sandbox. - Tool dirs under your home (
~/.cargo/bin,~/go/bin, npm globals) are deliberately not shared — the agent can't reach into your home. Install those in the agent account, or use the binary-copy route.
Everything jentic-owned is XDG on both sides (localagent keeps nothing in
~/.jentic — records written there by older releases are adopted into the XDG
file on the next jentic run/jentic reset write, or by jentic migrate):
| Location | Owner | Holds |
|---|---|---|
~/.config/jentic/agent-account.yaml (yours) |
you | The agent_account: record — account name, home, and the consolidated granted_dirs list — plus the one-time same-user notice flag. Paths and names only, no secrets. |
~/.config/jentic + ~/.local/state/jentic (yours) |
you | Your V2 identity store: environments, identities, contexts, keys, tokens. |
<agent home>/.config/jentic + <agent home>/.local/state/jentic |
agent | A minimal export of your active context (one environment + identity + context, mode forced to agent), refreshed at every launch — so the jentic the agent runs resolves the same install and identity without flags, and can never drift onto a stale copy. |
The ACLs on disk are the source of truth for access; the granted_dirs list is
the recorded inventory that --list-grants, the sandbox profile, and reset
read. Because the list lives in your config tree — and ~/.config/jentic is
itself hard-banned from grants and denied by the sandbox — the agent can't edit
its own access.
jentic reset # full clean slate (interactive, typed confirmation)
jentic reset --force # non-interactive; keeps the agent home
jentic reset --force --delete-home # non-interactive; deletes the home tooRun it as yourself, not sudo jentic reset — it prompts for your password
only when it reaches the privileged steps. It surveys first and shows the full
plan (every ACL, file, and the account) before a typed reset confirmation, and
then removes, in order: the directory ACLs (leaf grants and the ancestor
traverse entries), the agent's own jentic identity (its exported keys and
tokens under <agent home>/.config/jentic and <agent home>/.local/state/jentic,
plus any legacy <agent home>/.jentic), the sudoers drop-in, the Unix account,
and finally your own jentic identity state.
The agent's home is preserved by default — re-owned to you so the agent's
work survives — with its jentic identity and any seeded agent/provider config
(~/.claude, ~/.aws, ~/.codex, …) cleared from it, so no credential
material outlives the account. Deleting the home takes a separate, explicit
confirmation. To remove a single context or identity instead of everything, use
jentic context delete / jentic identity delete.
| Symptom | Cause | Fix |
|---|---|---|
confined agent sessions aren't available on this machine (CONFINEMENT_UNAVAILABLE) |
No sandbox-exec (macOS) or missing bwrap / unprivileged user namespaces / acl package (Linux) |
Run the printed install command, e.g. sudo apt install bubblewrap acl; userns: sudo sysctl -w kernel.unprivileged_userns_clone=1. jentic run never falls back to an unconfined session. |
| Password prompt on every launch | No passwordless rule installed | Re-run jentic setup and accept the passwordless-launch toggle, or enable Touch ID for sudo on macOS. |
agent account "…" does not exist |
Isolation was never set up (or was reset) | jentic setup (or jenticctl wizard). |
| "Running the agent as YOU" warning | Same-user fallback: no isolated account recorded | Expected if you declined isolation; jentic setup to isolate. |
| Grant prompt reappears for a directory you granted before | The ACL drifted (e.g. the directory was recreated by a build) | Re-grant; jentic run <agent> --list-grants shows the recorded inventory. |
| Agent can't see a file next to its workspace | Working as designed — only granted subtrees are visible | Grant the sibling directory too, or a common parent. |
test -w fails / editor save fails inside a granted dir on macOS |
A grant made by an older build used a narrower ACE set | Re-granting (or re-running setup) re-applies the full ACE set. |
| Noisy per-file errors during reset / account creation on macOS | SIP/TCC-protected home-template files (Library/Mail, …) that even root can't touch |
Harmless; the steps are best-effort by design and continue. |
Agent's jentic says it has no identity |
No active context at launch time, or env-var mode (JENTIC_BASE_URL/JENTIC_BEARER_TOKEN) |
Activate a context (jentic context use …) and relaunch — the export happens at every launch. |
| macOS | Linux | Windows | |
|---|---|---|---|
| Sandbox | Seatbelt (sandbox-exec, ships with the OS) |
bwrap + unprivileged userns + acl package |
❌ jentic run isolation unsupported — use WSL |
| Agent home | /Users/Shared/<agent> |
/opt/<agent> |
— |
| ACLs | chmod +a (native) |
POSIX ACLs (setfacl) |
— |
| Account tooling | sysadminctl / dscl |
useradd / userdel |
— |
jentic skill— writes the "how to use Jentic" skill set into your agent runtime's native layout (skill init/list/update/remove), so the launched agent knows how to drive the CLI.jentic setupruns it for you.jentic doctor— read-only self-check, including whether the agent would run as the same uid as you.jenticctl wizard— the installer-side onboarding; its agent-isolation step is the same setup flow described here.
- Security design index — the problem analysis, the full isolation design, the filesystem access model, and the confinement layer, including the residual risks and the mechanisms' exact commands.