Security Implementation
This document describes jackin's security implementation from a contributor perspective. It covers the threat model, the specific functions that enforce security invariants, the external tools bundled in the construct image, and the high-autonomy launch flags used for all five agent runtimes.
Threat model
| Threat | Defended? | Where in code | Residual risk |
|---|---|---|---|
| Agent reads/writes files on host | Yes | Mount scoping in crates/jackin-config/src/mounts.rs | Explicitly mounted rw paths |
| Credential theft via symlink | Yes | reject_symlink in crates/jackin-runtime/src/instance/auth.rs | TOCTOU window between symlink check and file use |
| Container escape | Out of scope | Docker hardening tracked in Docker runtime hardening contract and smolvm backend research | Shared kernel; future microVM work addresses this |
| Agent exfiltration over network | Needs policy | Tracked in Network egress policy | Open egress by default today |
| Supply chain (capsule binary) | SHA256 + signed manifest | crates/jackin-image/src/capsule_binary.rs | See signed releases |
Security functions
All six functions live in crates/jackin-runtime/src/instance/auth.rs and are called during credential provisioning at every jackin load / jackin console launch.
reject_symlink (line 925)
Uses symlink_metadata (lstat, not stat — the call does not follow the symlink) to detect symlinks at path before any write or chmod. An agent-controlled role-state directory is mounted read-write into the container; a compromised role can plant a symlink between launches to redirect credential writes to arbitrary host paths. reject_symlink is called unconditionally before every write path and at the top of every provision_* function. No-ops on ENOENT. Returns an operator-readable error naming the path and suggesting the operator remove the symlink and retry.
repair_permissions (line 1012)
Tightens existing credential files to 0o600 on Unix via set_permissions. Uses symlink_metadata (lstat) before calling set_permissions so it never chmods through a symlink. Errors (permission denied, stat failure) are logged via eprintln! rather than returned — callers are mid-Sync and must not abort the launch, but a silent chmod failure on a credential file is a security regression that must surface.
write_private_bytes / write_private_file (lines 951, 947)
Atomically writes bytes with 0o600 permissions. Calls reject_symlink at entry. Uses tempfile::NamedTempFile (opens with O_EXCL internally, so a pre-planted symlink at the temp path is impossible because O_EXCL fails on any existing path) with a random suffix, sets permissions on the temp file before rename. Closes the TOCTOU window completely: no intermediate state where a world-readable file exists at the destination path.
create_private_file_if_absent (line 986)
Race-free skeleton seeding via O_CREAT|O_EXCL. Used to seed {} into account_json before the Claude CLI runs so the downstream consumer never sees a missing file. Returns Ok(()) on EEXIST without touching existing content. The O_EXCL flag prevents any race between a missing-file check and the create.
host_home_is_real (line 231)
Gates host-binary shellouts (gh auth token, macOS security keychain) on whether host_home matches the value returned by directories::BaseDirs::new().home_dir(). Keeps integration tests hermetic (they pass temp dirs as host_home) while allowing the real gh and security binaries in production runs. Without this gate, tests would shell out to the operator's installed gh and contaminate results.
parse_gh_hosts_yml (line 346)
Uses serde_yaml_ng to parse ~/.config/gh/hosts.yml. Returns None on any malformed input, including a document that has a github.com block but no oauth_token field, or one where oauth_token is empty/whitespace. Partial results are never returned: silently accepting half-parsed scalars would land bogus credentials in the container's hosts.yml and surface as unrelated authentication failures mid-session.
External security tools
Both tools are installed in the construct image and active in every container session by default.
shellfirm
Intercepts dangerous shell commands at the shell level before execution, blocking rm -rf /, chmod 777, curl | bash, and similar patterns. Installed in docker/construct/Dockerfile at line 5 via cargo install shellfirm --version ... --locked in a multi-stage build. Shell hook wired in docker/construct/zshrc and docker/construct/fish-config.fish.
Protects against both accidental operator mistakes and agent-generated destructive commands. Disable per-session by setting JACKIN_DISABLE_SHELLFIRM=1 in the container environment.
tirith
Security scanner that validates container runtime state against a policy. Guards against configuration drift and unexpected privilege escalation during a session. Downloaded in docker/construct/Dockerfile at line 51 via a SHA256-verified curl + tar extraction. Shell hook wired in the same shell config files as shellfirm.
Disable per-session by setting JACKIN_DISABLE_TIRITH=1 in the container environment.
High-autonomy launch flags
jackin' launches each agent with flags that remove tool-approval prompts. These are defined in docker/runtime/entrypoint.sh and applied at container start before the agent binary runs.
| Agent | Configuration | Where |
|---|---|---|
| Claude | --dangerously-skip-permissions CLI flag + skipDangerousModePermissionPrompt: true in settings JSON | line 41 |
| Codex | --dangerously-bypass-approvals-and-sandbox CLI flag | line 47 |
| Amp | --dangerously-allow-all CLI flag | line 53 |
| Kimi | --yolo CLI flag (auto-approves all tool actions) | line 58 |
| OpenCode | OPENCODE_CONFIG_CONTENT='{"permission":"allow"}' env var + ~/.config/opencode/opencode.json written with "permission": "allow" by capsule runtime setup | line 64 + crates/jackin-capsule/src/runtime_setup.rs |
When any of these configurations are active: jackin' still enforces mount scoping, network isolation, and the host Docker socket exclusion. The flags remove tool-approval prompts but do not remove auth prompts, user questions, plan confirmation, model/account issues, runtime updates, or terminal-level failures. Operators who need the approval prompts restored can rebuild the derived image without the high-autonomy flags by customising the entrypoint hook.
Container boundary contract
The operator is responsible for what they mount. Explicitly mounted read-write paths are accessible to the agent — this is intentional and documented in the Mounts guide. The security model defends against the agent escaping the defined mount scope, not against the agent using what the operator chose to expose.
The YOLO flags above reduce friction at the cost of prompt-level damage prevention. The remaining guarantees (mount scoping, network isolation, no host Docker socket) hold regardless of flag state.