Skip to main content

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:
[src/example.ts#A1B2]
1:const value = 1;
2:console.log(value);
The four-character tag is a snapshot of the file content seen by the model. The edit tool accepts hashline scripts anchored to that tag:
[src/example.ts#A1B2]
replace 1..1:
+const value = 2;
insert tail:
+// done
Supported hashline operations include 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 or background-job handling. When pty: true is requested, local execution uses the bundled Rust-backed PTY session so commands see a real terminal, including headless/tool-only and async job calls; 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. Completed foreground results include oh-my-pi-style timeoutSeconds, requestedTimeoutSeconds, wallTimeMs, and non-zero exitCode metadata; background jobs use details.async: { state, jobId, type: "bash" }, can be polled with bash({"command":"__atomic_bash_job <id>"}), can be cancelled with bash({"command":"__atomic_bash_job_cancel <id>"}), and preserve overflow output in a temporary fullOutputPath when polling output is truncated. bashInterceptor.enabled defaults to false; interception is not auto-enabled. 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, pty, and async; cwd and env are honored by the local executor, timeout defaults to 300s and is clamped to 1..3600s, and normal sessions enable tracked async jobs with bounded retention.
{
  "bashInterceptor": { "enabled": true }
}
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, 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.