jinn confines every file operation to the working directory. You cannot read from, write to, or traverse into sensitive paths. There is no disable flag -- security is always on.
Every file path goes through two checks before any I/O:
resolvePathjoins the path to the working directory and callsfilepath.Clean. Symlinks in the working directory itself are resolved.checkPathresolves any symlinks in the requested path, verifies no sensitive segments are present, and confirms the final path stays within the working directory boundary.
# This is blocked -- .ssh is a sensitive segment
echo '{"tool":"read_file","args":{"path":"../.ssh/id_rsa"}}' | jinn{"ok": false, "error": "blocked: sensitive path: ../.ssh/id_rsa"}.. traversal, symlink escapes, and absolute paths that point outside the working directory are all blocked. The working directory is the root of all file access.
related_context has a narrow read-only exception: it scans shared local context directories such as ~/.claude/kb, directories listed in related_context.paths in ~/.config/jinn/config.json (or $JINN_CONFIG_DIR/jinn/config.json), and client-specific skill directories for the declared request client (claude, codex, or pi). It returns metadata and paths only, skips sensitive path segments, and does not weaken checkPath for file-reading or mutation tools.
checkPath rejects any path containing these segments:
| Segment | Reason |
|---|---|
.git |
Repository internals -- refs, hooks, config |
.ssh |
SSH keys and configuration |
.aws |
AWS credentials |
.gnupg |
GPG keyrings |
.env |
Environment variable files with secrets |
.env.* |
Variant environment files (e.g., .env.production) |
The check matches on path segments, so src/.env and deploy/.env.staging are both blocked regardless of depth.
jinn tracks file modification times to prevent time-of-check-to-time-of-use races. When you read a file, jinn records its mtime. When you write or edit that file, jinn checks whether the mtime has changed since the read. If it has, the write is rejected.
# Step 1: Read the file (jinn records mtime)
echo '{"tool":"read_file","args":{"path":"config.yaml"}}' | jinn
# Step 2: Edit the file (jinn verifies mtime hasn't changed)
echo '{"tool":"edit_file","args":{"path":"config.yaml","old_text":"port: 8080","new_text":"port: 9090"}}' | jinnIf another process modifies config.yaml between steps 1 and 2:
{"ok": false, "error": "file modified since last read (mtime changed). Re-read before writing: config.yaml"}Exceptions: New files (never read) and deleted files (stat fails) bypass the TOCTOU check. You can always create a new file or overwrite a deleted one.
The TOCTOU tracker is per-engine instance. Each jinn process starts fresh -- there is no global state persisted between invocations.
write_file, edit_file, and batch mutation tools all use the same per-file atomic write pattern:
- Write content to a hidden temp file (
.jinn-*prefix). chmodto match existing file permissions (or use default for new files).fsyncthe temp file to ensure durability.renamethe temp file to the target path.
echo '{"tool":"write_file","args":{"path":"data.json","content":"{\"status\":\"ok\"}\n"}}' | jinnIf the process crashes mid-write, the target file is never left in a partial state. The rename is atomic on all major filesystems. The temp file is cleaned up on error.
Batch mutation tools validate all inputs before writing, but they do not roll back earlier successful writes if a later per-file write fails.
Before executing any shell command, run_shell classifies it by examining the leading verb and flags:
| Level | Behavior | Examples |
|---|---|---|
safe |
Executed normally | ls, cat, grep, find, echo |
caution |
Executed normally; modifies state | cp, mv, mkdir, sed -i, curl, unknown verbs |
dangerous |
Blocked unless force: true |
rm, dd, sudo, kill, shutdown, pipe to sh/bash |
The risk field is always present in run_shell responses. Dangerous commands return an error with risk: "dangerous" and a suggestion unless force: true is set:
{
"ok": false,
"error": "blocked by risk classifier: dangerous — removes files — irreversible",
"suggestion": "pass force:true in args to override, or use a less-destructive command",
"risk": "dangerous"
}To override the block for a known-safe case:
echo '{"tool":"run_shell","args":{"command":"rm -rf /tmp/build-cache","force":true}}' | jinnPipelines return the maximum risk of any component (cmd1 | cmd2 inherits the higher classification). Pipe-to-shell (cmd | bash) is always dangerous. Unknown verbs default to caution, not safe.
run_shell does not inherit your full shell environment. jinn scrubs the environment down to an allowlist before executing the command:
| Variable | Why it's kept |
|---|---|
PATH |
Finds executables |
HOME |
User home directory |
LANG |
Locale |
LC_ALL |
Locale override |
TERM |
Terminal capabilities |
USER |
Current username |
LOGNAME |
Login name |
TMPDIR |
Temp directory |
TZ |
Timezone |
SHELL |
Shell path |
All other environment variables -- including any API keys, tokens, or secrets you have exported -- are removed before the command runs. This prevents accidental credential leakage through child processes.
jinn caps output to prevent unbounded memory growth:
| Boundary | Value | Applies To |
|---|---|---|
| Shell output buffer | 1 MB | run_shell |
| Per-line truncation | Truncated at rune boundary + ... |
All tools |
| Repeated line collapse | 3+ identical consecutive lines collapsed | All tools |
| Shell tail truncation | Last N lines kept | run_shell |
| Read truncation | Configurable strategy (head/tail/middle/none/smart); default keeps first N lines. smart uses brace-depth heuristic for C-syntax files, cutting at block boundaries. Truncation hint appended: [Showing lines X-Y of Z. Use start_line=N to continue. Remainder saved to <path>.] |
read_file |
| File size limit | 50 MB | read_file |
When shell output exceeds 1 MB, it spills to a temp file (jinn-shell-*.log). jinn keeps the tail of the output so you always see the exit code and final lines.
The repeated line collapse replaces 3 or more identical consecutive output lines with [... N identical lines collapsed ...]. This keeps build output and log dumps readable without losing the line count.
read_file applies type-specific handling before returning content:
| File type | Behavior |
|---|---|
.pdf |
Returns ok: false with suggestion: "convert the PDF to text first (pdftotext, pdftk, or a cloud OCR service) and read the text file" |
| Images | Detected by content (via http.DetectContentType on the first 512 bytes) rather than extension alone. A PNG renamed without an extension is still identified and handled correctly. Returns a base64-encoded content block with the detected MIME type. SVG files (which read as text/xml by the content detector) fall back to extension-based detection and return image/svg+xml. |
| Binary (null byte in first 512 bytes) | Returns [binary file: N bytes — use checksum_tree for integrity or skip content reads] (success, not error) |
The memory tool stores its data in a SQLite database at ~/.config/jinn/memory.db (or $JINN_CONFIG_DIR/jinn/memory.db when the env var is set). The directory is created with mode 0700. Writes use WAL journaling with a 5s busy_timeout, providing cross-process safety so concurrent jinn invocations cannot corrupt the store. Keys are isolated per project scope.
| Mechanism | Scope | Configurable |
|---|---|---|
| Path confinement | All file tools | No |
| Sensitive path blocking | All file tools | No |
| TOCTOU tracking | read_file records, mutation tools enforce where applicable |
No |
| Atomic writes | Per-file writes in mutation tools | No |
| Environment scrubbing | run_shell |
No |
| Risk classifier | run_shell |
force: true overrides dangerous block |
| Output bounds | All tools | No |
| Memory DB directory permissions | memory |
$JINN_CONFIG_DIR relocates storage |
Security in jinn is enforced at the engine level. Path confinement, sensitive path blocking, TOCTOU tracking, and environment scrubbing have no bypass. The risk classifier has one intentional override (force: true) for callers that have verified the command is safe for their context.