Skip to content

Latest commit

 

History

History
175 lines (122 loc) · 8.76 KB

File metadata and controls

175 lines (122 loc) · 8.76 KB

Security

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.

Path Confinement

Every file path goes through two checks before any I/O:

  1. resolvePath joins the path to the working directory and calls filepath.Clean. Symlinks in the working directory itself are resolved.
  2. checkPath resolves 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.

Sensitive Paths

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.

TOCTOU Protection

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"}}' | jinn

If 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.

Atomic Writes

write_file, edit_file, and batch mutation tools all use the same per-file atomic write pattern:

  1. Write content to a hidden temp file (.jinn-* prefix).
  2. chmod to match existing file permissions (or use default for new files).
  3. fsync the temp file to ensure durability.
  4. rename the temp file to the target path.
echo '{"tool":"write_file","args":{"path":"data.json","content":"{\"status\":\"ok\"}\n"}}' | jinn

If 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.

Command Risk Classifier

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}}' | jinn

Pipelines 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.


Shell Environment Scrubbing

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.

Output Bounds

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.

Special File Reads

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)

Memory Persistence

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.


Summary

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.