Built-in tools
Atomic enables these coding tools in normal sessions by default:read, write, edit, bash, find, and search.
Hashline editing anchors
read, search, write, and successful edit results for local text files emit an editable hashline header:
edit tool accepts hashline scripts anchored to that tag:
replace N..M:, replace block N:, delete N..M, delete block N, insert before N:, insert after N:, insert after block N:, insert head:, and insert tail:. Safe lenient variants such as replace N, replace N-M:, replace N M:, replace N…M:, bare body rows, and *** Begin Patch envelopes are accepted. Bare body rows are auto-prefixed and reported as warnings. *** Abort stops parsing the remaining input, while apply-patch sentinels, @@ hunk headers, bare numeric hunk headers, delete bodies, empty replace/insert bodies, and - diff rows are rejected with guidance instead of silently deleting content. Line numbers refer to the original tagged snapshot and do not shift within a call.
Before writing, Atomic verifies the current file against the tagged snapshot. If the file drifted, edit first attempts a snapshot-based recovery for provably non-overlapping external changes and appends a warning when it preserves those changes; unknown tags, overlapping stale edits, and unrecoverable drift fail clearly with the current file hash (and anchor context for drifted files) and leave the file unchanged. Byte-identical no-op edits return a no-op warning without writing, and repeated identical no-ops escalate to an error to stop looped retries. Hashline snapshots are scoped to the active tool/session store, so tags emitted in another session or stale context do not authorize edits. One edit input may contain multiple [PATH#TAG] sections; Atomic preflights every section before writing, but this is preflight atomicity rather than transactional rollback, so a mid-batch filesystem write failure can leave earlier sections already written. Each successful write or edit returns a fresh tag for follow-up edits; hashline edit success output is compact and includes the refreshed header plus block-resolution/change metadata while the full diff remains in tool details. Plain write success output is likewise compact ([path#TAG] plus a byte-count summary), not a full reprint of the file. write strips copied hashline headers and LINE:/*LINE: display prefixes only when the pasted content matches a known current-store snapshot and notes when stripping occurred. Literal or unknown hashline-looking content is preserved instead of being stripped.
bash and bashInterceptor
The bash tool executes shell commands in the session workspace, with optional PTY handling. When pty: true is requested, local execution uses the bundled Rust-backed PTY session; if the native PTY package is unavailable, Atomic degrades to normal pipe execution. Set PI_NO_PTY=1 or ATOMIC_NO_PTY=1 to force normal pipe execution. Foreground results include oh-my-pi-style timeoutSeconds, requestedTimeoutSeconds, wallTimeMs, and non-zero exitCode metadata, and preserve overflow output in a temporary fullOutputPath when output is truncated.
When explicitly enabled in settings, built-in bash interceptor rules block common shell substitutes for first-class tools (cat/grep/find/in-place sed/redirection, etc.) only when the corresponding tool is available. Enabled bash tool calls are also offered to user_bash extension handlers before local execution. Atomic checks the original command, the internal-URL-expanded command, configured-prefix forms, spawnHook-rewritten commands, and a leading cd path && command or cd path; command-stripped form only when structured cwd was omitted, so interceptors can route commands by effective working directory without overriding explicit cwd. The bash schema accepts cwd, env, timeout, and pty; cwd and env are honored by the local executor. Omitting timeout uses the 300-second default. An explicit timeout must be finite, greater than zero, and no more than Atomic’s deliberate 3600-second ceiling; invalid values fail before execution instead of being defaulted or clamped. Valid fractional values are rounded down with a one-second floor. bashInterceptor.enabled defaults to false; interception is not auto-enabled.
find and search
find finds filesystem paths by glob; use search when you need content matches instead of path matches. find.paths is required and accepts file, directory (including a filesystem root), internal URL, or glob paths; copied quoted paths are normalized, exact filesystem paths with spaces/commas/semicolons are preserved before delimiter expansion, comma/semicolon-joined paths are split when at least one part resolves, whitespace-joined paths are split only when every part resolves, hidden files are included by default, .gitignore is respected by default (including nested .gitignore files outside a Git checkout), broad scans keep node_modules/.git pruned even with gitignore:false unless node_modules is explicitly present in the requested path or glob, results are capped at 200 by default, and timeout defaults to 5 seconds. Local find prefers the bundled Rust native glob implementation derived from oh-my-pi, falling back to the packaged fd helper only when native bindings are unavailable. Results include scopePath, fileCount, files, truncation/missing-path metadata, and streamed onUpdate snapshots during long scans.
search searches file contents with a regex across files, directories, globs, archive members, SQLite selectors, and internal URLs. It accepts pattern, optional paths, i, case, gitignore, and skip. Omitted, empty-string, or empty-array paths search the workspace root; copied quoted paths are normalized, exact filesystem paths with delimiters are preserved, and comma/semicolon-joined filesystem/resource paths are split when at least one part resolves while whitespace joins require every part to resolve. Whitespace-only patterns are rejected; other patterns are preserved verbatim, including ripgrep-style inline flags such as (?i), (?m), and (?x) for resource-backed selectors. Default search output is paged by matching files (20 by default), caps multi-file output to 20 matches per file while single-file searches allow 200 matches, uses search.contextBefore/search.contextAfter settings (1 before and 3 after by default), and skip pages files while single-file searches ignore it. When pagination reaches the internal collection ceiling, the output tells you to refine the pattern/path instead of silently reporting that later matches do not exist. Line selectors scope matches first and then render context around in-range matches, so context-only lines outside the selected range do not count as hits. Local filesystem search prefers the bundled Rust ripgrep-backed native grep implementation derived from oh-my-pi, preserving upstream 4 MiB file caps, context, line truncation, hidden-file defaults, and .gitignore handling; resource-backed searches use the native in-memory matcher when available and keep the JS fallback for multiline/resource edge cases. Search details include scope, counts, file lists, per-file match counts, missing paths, displayed content metadata, and fileLimitReached/meta.limits.fileLimit when pagination has more matching files. Hashline search rows preserve match/context markers as *LINE:... and LINE:....
Directory read output renders an oh-my-pi-style depth-2 tree sorted by most-recent modification time, includes file sizes/relative ages, prunes .git/node_modules, and caps child directories to 12 entries with an elision marker while preserving the oldest shown entry. read, write, and search support local zip/jar/tar/tgz/gzip archive members without a Python dependency, including archive members literally named raw, conflicts, 1, L1, or paths like raw:notes.txt, SQLite table/row selectors (limit, offset, where, order, schema, and sampleRows query parameters), skill:// and source-backed local:// selectors (which use the underlying filesystem path for mutable hashline labels/snapshots), and session-router-backed internal resources such as artifact://, agent://, history://, issue://, pr://, rule://, mcp://, and vault:// when the host exposes a router. Workspace-scoped selectors (local://, built-in skill://, local archives, and SQLite selectors) reject lexical and symlink escapes outside the workspace or skill root. Existing non-SQLite .db/.sqlite files remain plain files; archive writes reject directory targets ending in /; archive writes return their resolved archive path, SQLite writes return source-path metadata, shebang writes are chmodded executable and report madeExecutable, SQLite table reads show schema plus a 5-row sample by default, SQLite query reads default to 20 rows with a 500 cap (raw ?q= supports single-statement SELECT queries only, rejects sqlite_% internals, pragma_* table-valued functions, and dangerous keywords such as ATTACH, and is capped to 1000 rows via streaming iteration; table lists cap to 500 excluding sqlite_% tables, and table row counts probe at most 50,001 rows), table writes accept {} as INSERT DEFAULT VALUES, row writes parse non-empty JSON5-style objects including comments, and SQLite writes validate column names/scalar values before binding (empty SQLite row writes delete only when a row id is present). conflict://<id> and conflict://* writes splice conflict marker regions, expand @ours, @theirs, and @base, and return fresh hashline snapshot headers for resolved files; scoped conflict sides such as conflict://1/ours are read-only. Plain write refuses to overwrite generated-looking files when generated markers appear near the top of the file. read extracts readable text for HTML URLs and notebooks (.ipynb cells use 0-based cell:N IDs and preserve unknown top-level notebook fields), and routes PDFs plus Office/document formats (.doc, .docx, .ppt, .pptx, .xls, .xlsx, .rtf, .epub) through the same markit-ai converter path as oh-my-pi, including upstream unsupported-format messages when no converter is available. Extensionless URL downloads are decoded when the Content-Type identifies the document type. Oversized URL/resource/document reads return guidance plus structured details so collapsed renderers still surface the block reason. Successful read results consistently return details.meta.source/sourcePath (plus truncation/limits when truncation or list limits apply), and read/search accept line selectors such as file.ts:5-16, file.ts:5+3, file.ts:5-16,960-973, or https://example.test/page:5-8; bounded read selectors include one leading and three trailing context lines unless :raw is used for unformatted content, and out-of-range selectors report a clear beyond-EOF message instead of returning an empty success.
Plain URL reads follow oh-my-pi’s fetch-pipeline truncation contract: unselected URL output shows the first 300 rendered lines (capped at 50 KiB), preserves full-output artifact/truncation metadata when available, and does not hard-block solely because the rendered URL body is large. By default Atomic rejects private, localhost, cloud-metadata, numeric/short-form private-IP URL targets (for example 2130706433, octal/hex dotted forms, and 127.1), IPv4-compatible and IPv4-mapped IPv6, NAT64, 6to4 private-address forms, and the full IPv6 link-local fe80::/10 range, revalidates each manual redirect, pins DNS-validated addresses for outbound fetches, and caps streamed URL bodies; ATOMIC_ALLOW_PRIVATE_URL_READS=1 is a dev-only escape hatch for trusted local tests and must not be set from untrusted project configuration. Local text reads use the shared 3,000-line/50 KiB output cap, while search match/context lines use the upstream 512-character cap before emitting a truncation notice.
Persisted tool output
Output that does not fit in a tool result is written to a file, and the result points at it —Full output: <path> for bash, Full output saved to: <path> for any tool result that crosses the persistence threshold. Those files are storage, so Atomic bounds where they go, how large they get, and how long they live.
Where. A session that persists to disk keeps its tool results in <sessionDir>/tool-results/, unchanged. Everything else — bash overflow logs, streamed spill files, and tool results for in-memory sessions — goes under one owner- and session-scoped temp tree:
CONTOSO\Alice and FABRIKAM\Alice, or two names that reduce to the same safe path component, do not share one tree. The session id is reduced to a single safe path component, so a session id containing separators or .. cannot place files outside that tree.
Directories are created with 0700 and files with 0600 on platforms that support them. On POSIX that is enforced: a root owned by another account is refused and a too-permissive one is tightened. On Windows, Node exposes neither an owner SID nor a POSIX mode, so before adopting an existing component Atomic reads its security descriptor through PowerShell Get-Acl and decides from SIDs: the owner must be the current user, SYSTEM, or Administrators, and every access-allowed DACL entry must grant only those principals. A foreign-owned root, a root other accounts can access (a machine-wide redirected TMP), and a descriptor that cannot be read or fully parsed are all refused. A successful verification is cached per path per process, keyed to the directory’s metadata identity — creation, change, and last-write times — so both a directory swapped underneath the path and an in-place DACL edit (which bumps the NTFS change time) force a fresh descriptor read; only a directory whose metadata is unchanged skips the subprocess.
Because that path is predictable, Atomic validates it rather than trusting it. Every component below the system temp directory is created one level at a time and checked: a symlink is refused outright, a directory owned by another account is refused, and a directory of this account’s left too permissive is tightened to 0700 and re-checked. Refusal fails closed — the tool runs with no spill file and reports no path, instead of writing your command output into a directory someone else planted. Only the final session directory is treated as replaceable: a stale file or link sitting exactly where a session directory belongs is removed (a link by itself, never its target) and recreated.
How large. No single persisted-output file exceeds 64 MB. Output that lands exactly on the cap is kept whole; only output that passes it is cut off there, with [Output truncated: persisted-output cap of 67108864 bytes reached] appended in place of the rest. The cut lands on a character boundary even when a multi-byte character is split across two chunks of streamed output, and bytes that are not valid UTF-8 — binary command output — are written through unchanged rather than decoded. The truncation is in the file only; the tool result and its fullOutputPath are unaffected. If a spill file cannot be written at all, bash reports no path rather than one pointing at a file that is not there.
How long. Tool-output storage is reaped on age. A sweep runs shortly after startup, off the startup path, and removes session temp trees and tool-results directories whose newest file is more than 30 days old. It covers the default project-nested session roots and a custom session directory chosen with --session-dir, ATOMIC_CODING_AGENT_SESSION_DIR, or the sessionDir setting. The newest entry decides the whole directory: one fresh file keeps everything beside it, so a path recorded weeks ago stays readable as long as the tree is still in use. Each target is throttled to once a day by a .last-cleanup marker and guarded by a .cleanup.lock exclusive lock, so concurrent Atomic sessions never race each other; for session storage that pair lives in a control directory under the temp root rather than inside the scanned directory. A tree belonging to a session the running process is using is never reaped, so a Full output saved to: path stays valid for the life of the session that produced it. Session transcripts and .jsonl session files are outside the sweep entirely: only the temp trees and tool-results directories are ever deleted, symlinks are never followed out of a target, and a directory the sweep cannot verify as a real directory is skipped rather than read.