# 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:

```text
[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:

```text
[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.

When a session-managed background bash job completes or fails, Atomic sends an `async-job-result` custom follow-up into the conversation automatically (`display: true`, delivered as a follow-up turn). Small results are inlined, and results whose raw output stays below the persistence threshold remain fully inline even if the formatted follow-up header pushes the message over the preview limit; persisted large results include a preview plus the retained `fullOutputPath` (persisted before the normal polling truncation limit so 12KB–50KB outputs remain recoverable). If the model explicitly polls a completed job with `__atomic_bash_job <id>` before the queued follow-up is delivered, or cancels a job with `__atomic_bash_job_cancel <id>`, Atomic acknowledges the result and suppresses duplicate auto-delivery while keeping the job pollable until normal bounded retention/TTL cleanup. Suppression is tied to the retained job rather than a short timer, so disposed-session jobs cannot later fall back into another session after a long-running command completes. Session delivery attempts are non-blocking across sessions: a live streaming session can defer its own follow-up until the stream boundary without delaying unrelated completed jobs. Session disposal removes that session's pending async delivery handlers; a shared manager remains alive while other live sessions still own active jobs, then cleans up when the last session is disposed. Direct SDK/tool-factory uses only get automatic delivery when they provide an async job manager/delivery handler; otherwise async jobs remain manually pollable.

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. 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. Normal sessions enable tracked async jobs with bounded retention. `bashInterceptor.enabled` defaults to `false`; interception is not auto-enabled.

```json
{
  "bashInterceptor": { "enabled": true }
}
```

## `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, 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.
