List
Feature Definition
Section titled “Feature Definition”A list tool lets the model inspect filesystem structure without opening file contents.
It is the lowest-cost discovery primitive in the tool stack:
- identify candidate files before
read - inspect directory shape before
glob - confirm rename/move results after edits
- build an internal map of a repo without paying content-token costs
In real agent workflows, list is often the first tool called after a user drops into a new directory. Everything after it (grep, mapping, edit planning, “what files exist?”) depends on the list tool being correct and predictable.
Why Directory Listing Is Hard
Section titled “Why Directory Listing Is Hard”A production list tool has to balance competing constraints:
- completeness vs token budget (huge trees can flood context)
- speed vs correctness (deep recursive scans can be slow)
- safety vs utility (paths must stay inside allowed roots)
- determinism vs platform differences (sorting, separators, hidden files)
- semantics (VCS-tracked listing vs filesystem listing are different products)
If listing is noisy or incomplete, the agent will “plan on ghosts”: it will believe files exist or do not exist based on truncated or filtered output.
Minimal List Requirements
Section titled “Minimal List Requirements”A list tool is “good enough” when it:
- returns deterministic ordering for stable reasoning
- indicates truncation explicitly
- exposes a way to page/cursor
- has clear ignore rules
- has a clear safety boundary for what roots can be listed
- is cheap enough to call frequently
See Also
Section titled “See Also”- Glob and Grep for narrowing candidate paths after listing.
- File Read for content retrieval after discovery.
- Repo Mapping for structural summaries when a raw listing is not enough.
Aider Implementation
Section titled “Aider Implementation”Aider at commit b9050e1d5faf8096eae7a46a9ecc05a86231384b does not expose a model function-call list tool.
Listing exists as a slash command (/ls) executed by the host program, not as a tool schema the model calls.
/ls Command Entry Point
Section titled “/ls Command Entry Point”Aider implements listing in references/aider/aider/commands.py:
Commands.cmd_ls(self, args)- invoked from
Commands.run()after command parsing
Core behavior (abridged) from cmd_ls:
def cmd_ls(self, args): files = self.coder.get_all_relative_files()
other_files = [] chat_files = [] read_only_files = []
for file in files: abs_file_path = self.coder.abs_root_path(file) if abs_file_path in self.coder.abs_fnames: chat_files.append(file) else: other_files.append(file)
for abs_file_path in self.coder.abs_read_only_fnames: rel_file_path = self.coder.get_rel_fname(abs_file_path) read_only_files.append(rel_file_path)
...The key insight: Aider’s list is primarily a view of “files Aider knows about” rather than an arbitrary filesystem directory walk.
File Enumeration Pipeline
Section titled “File Enumeration Pipeline”The list of “known files” comes from Coder.get_all_relative_files() in references/aider/aider/coders/base_coder.py:
def get_all_relative_files(self): if self.repo: files = self.repo.get_tracked_files() else: files = self.get_inchat_relative_files() return sorted(set(files))If Aider is in a git repo, listing is derived from repo.get_tracked_files().
This is not a directory scan. It is a git-tree traversal plus staged-file augmentation.
Git Tree Traversal and Caching
Section titled “Git Tree Traversal and Caching”GitRepo.get_tracked_files() in references/aider/aider/repo.py:
- tries
self.repo.head.commit - traverses
commit.tree.traverse()to collect blobs (files) - caches results per commit in
self.tree_files[commit] - adds staged files from
self.repo.index.entries - normalizes paths via
normalize_path() - applies Aider-level ignore rules via
ignored_file()
Important behaviors that impact correctness:
- if HEAD cannot be resolved (empty repo), traversal path is skipped and listing relies on staged files and other sources
- caching is commit-based: changing branches changes the key; changing working-tree files does not
- path normalization uses
PurePosixPathlogic so output paths are forward-slash normalized
.aiderignore and Subtree Filtering
Section titled “.aiderignore and Subtree Filtering”Aider applies its own ignore layer in GitRepo.ignored_file() and ignored_file_raw().
Mechanics (from references/aider/aider/repo.py):
refresh_aider_ignore()checks the.aiderignorefile at most once per second- on mtime change, it rebuilds a
pathspec.PathSpecusingGitWildMatchPattern ignored_file()caches per-filename results inignore_file_cache
Subtree mode:
- when
subtree_onlyis enabled, Aider rejects files outside the current working subtree - this check happens before
.aiderignorematching and can drop paths even if tracked
Output Buckets and UX
Section titled “Output Buckets and UX”Aider’s /ls is not just a raw list; it is a session-centric view split into:
- “Repo files not in the chat” (tracked but not in the current chat context)
- “Read-only files” (
abs_read_only_fnames) - “Files in chat” (present in
abs_fnames)
This is a useful UX for interactive editing, because it answers “what does the model currently see?”
Operational Constraints
Section titled “Operational Constraints”Because listing is not tool-callable:
- there is no structured, model-visible list schema
- there is no stable pagination contract
- the model cannot ask for “depth=2” or “offset=51”
- it is not safe to treat
/lsoutput as a complete directory enumeration
Aider’s list is a VCS-centric file inventory, designed for chat session management.
Codex Implementation
Section titled “Codex Implementation”Codex at commit 4ab44e2c5cc54ed47e47a6729dfd8aa5a3dc2476 exposes a dedicated tool named list_dir.
Codex’s list tool is a real model-callable function tool with a typed argument schema and deterministic formatting.
Tool Registration and Gating
Section titled “Tool Registration and Gating”Tool spec is defined in references/codex/codex-rs/core/src/tools/spec.rs as create_list_dir_tool().
Key properties:
- tool name:
list_dir - required parameter:
dir_path - optional parameters:
offset,limit,depth - semantics: 1-indexed entry numbering
list_dir is not always enabled. It is registered when experimental_supported_tools contains "list_dir".
In the tool builder section (same file), Codex registers the spec and handler:
builder.push_spec_with_parallel_support(create_list_dir_tool(), true);builder.register_handler("list_dir", list_dir_handler);
This also explicitly marks list_dir as safe for parallel tool calls.
Argument Contract and Validation
Section titled “Argument Contract and Validation”The handler is ListDirHandler in references/codex/codex-rs/core/src/tools/handlers/list_dir.rs.
Arguments:
dir_path: Stringoffset: usizedefault 1limit: usizedefault 25depth: usizedefault 2
Validation rules enforced in code:
offsetmust be >= 1 (rejects 0)limitmust be > 0depthmust be > 0dir_pathmust be an absolute path
Errors are returned as FunctionCallError::RespondToModel(...) with user-visible text.
Directory Walk and Pagination Flow
Section titled “Directory Walk and Pagination Flow”Codex’s directory walk is implemented in list_dir_slice() and collect_entries().
Core steps:
collect_entries(path, "", depth, entries)builds a flatVec<DirEntry>entries.sort_unstable_by(|a, b| a.name.cmp(&b.name))sorts by normalized path string- slice selection uses 1-indexed
offsetand boundedlimit - format selected entries with indentation derived from prefix depth
- append a truncation line if there are more entries
There are two separate sorts:
- within each directory level, the handler sorts entries before pushing them into the global list
- it also sorts the global list by
DirEntry.name(a normalized relative path string)
This yields stable tree-like ordering such as:
nested/child.txtdeeper/grandchild.txt
Output Format
Section titled “Output Format”The tool output begins with a header:
Absolute path: <dir_path>
Then each entry is printed as a single line.
Formatting details:
- indentation: 2 spaces per directory depth
- directory entries have
/suffix - symlinks have
@suffix - other/non-file/non-dir entries have
?suffix - filenames and paths are truncated to 500 characters (
MAX_ENTRY_LENGTH) at character boundaries - Windows backslashes are normalized to
/in the internal sort/print name
Truncation line:
- if selection ends before the full list ends, it appends
More than {capped_limit} entries found
Important nuance:
capped_limitis the number returned in the page, not the total entry count
Depth Semantics
Section titled “Depth Semantics”depth controls traversal depth:
depth=1lists only immediate childrendepth=2includes children of subdirectories- traversal uses a queue and only traverses entries where
kind == Directory && remaining_depth > 1
Symlink behavior:
- symlink entries are printed with
@ - symlinks are not traversed because only
DirEntryKind::Directoryis recursed
This avoids the worst class of symlink loops by default.
Test Coverage
Section titled “Test Coverage”references/codex/codex-rs/core/src/tools/handlers/list_dir.rs includes unit tests that verify:
- directories, files, and (on Unix) symlinks are rendered correctly
offsetout of bounds returns a model-visible error- depth boundaries produce expected entry sets
- pagination preserves sorted order
- large limits and truncation behavior
The tests are valuable documentation: they encode the formatting and ordering contract.
Implementation Notes
Section titled “Implementation Notes”Practical characteristics and tradeoffs:
- the handler collects all entries up to
depthbefore slicing; large trees can still be expensive even with smalllimit - output is plain text; there is no structured return type with per-entry metadata (size, mtime)
- absolute paths are required; this pushes path resolution responsibility to the model/host
OpenCode Implementation
Section titled “OpenCode Implementation”OpenCode at commit 7ed449974864361bad2c1f1405769fd2c2fcdf42 implements a tool named list in references/opencode/packages/opencode/src/tool/ls.ts.
The implementation is intentionally shaped around ripgrep’s fast file enumeration.
Registry and Exposure
Section titled “Registry and Exposure”list is built using Tool.define("list", ...) and discovered like other built-in tools.
The registry pipeline in references/opencode/packages/opencode/src/tool/registry.ts:
ToolRegistry.all()builds a list of built-in tools plus custom/plugin toolsToolRegistry.tools(model, agent?)filters tool exposure per provider and model- tool definitions are exposed to the model via
{ description, parameters } - plugins can mutate tool definitions via
Plugin.trigger("tool.definition", ...)
The list tool is always present in all() (not feature-gated), unlike OpenCode’s commented-out TodoReadTool example.
Permission and Path Guardrails
Section titled “Permission and Path Guardrails”OpenCode uses two relevant gates:
assertExternalDirectory(ctx, searchPath, { kind: "directory" })
- if
searchPathis outside the active instance directory, it asks permissionexternal_directoryforparentDir/* - this is an “escape hatch” guardrail so tools don’t silently traverse arbitrary filesystem roots
ctx.ask({ permission: "list", patterns: [searchPath], ... })
- explicit permission for listing the target directory
So listing a directory outside the project can require two separate approvals.
Enumeration and Rendering
Section titled “Enumeration and Rendering”The list tool does not call Node’s fs.readdir recursively.
Instead it uses Ripgrep.files:
- spawns
rg --files --glob=!.git/* - defaults to
--hiddenunlesshidden: false - supports additional
--glob=...filters - yields each file path as a streaming async iterator
ListTool constructs ignore globs by turning patterns into --glob=!<pattern> forms.
Defaults:
IGNORE_PATTERNSincludesnode_modules/,.git/,dist/,target/, and many moreLIMITis 100 paths
The loop is explicitly capped:
- it breaks once it has collected 100 files
Then it builds a directory structure from returned file paths:
- collects
dirsby adding all parents of each file - groups file basenames by directory path (
filesByDir) - renders a tree with
renderDir(dirPath, depth)
Output:
- begins with
<searchPath>/ - then renders subdirectories (alphabetical) then files (alphabetical)
Metadata:
count: number of files collected (<= 100)truncated: true iffiles.length >= LIMIT
CLI/TUI Integration
Section titled “CLI/TUI Integration”OpenCode renders tool execution uniformly:
- CLI run mode prints tool title/output
- TUI renders tool blocks in the session timeline
list returns both output and metadata, enabling UI to show “truncated” hints and counts.
Pitfalls & Hard Lessons
Section titled “Pitfalls & Hard Lessons””List” Often Means Different Things
Section titled “”List” Often Means Different Things”Aider’s /ls is not a filesystem directory list; it is a git-tracked inventory plus staged files.
Codex/OpenCode list actual directory contents.
If OpenOxide wants compatibility with all three mental models, it must name the semantics explicitly.
Truncation Must Be Loud
Section titled “Truncation Must Be Loud”OpenCode caps at 100 files. Codex paginates but still has traversal cost. Aider lists tracked files and can omit untracked paths entirely.
All three can produce incomplete views.
A correct list tool must:
- indicate truncation in the output and in structured metadata
- expose a paging/cursor mechanism
Ignore Rules Change the Product
Section titled “Ignore Rules Change the Product”Ignoring node_modules/ and .git/ is usually correct, but:
- monorepos sometimes keep source-of-truth artifacts under
vendor/orthird_party/ - “dist/” and “build/” sometimes contain generated sources worth inspecting
- ignoring hidden files by default can hide important config (
.env.example,.tool-versions,.cargo/config.toml)
OpenOxide should treat ignore policy as configurable, not hard-coded.
Symlink Loops and Filesystem Hazards
Section titled “Symlink Loops and Filesystem Hazards”Codex avoids traversing symlinks by only recursing FileType::is_dir() entries.
If OpenOxide traverses symlinks, it must:
- track visited inode/device pairs
- enforce maximum traversal depth
- enforce timeouts and cancellation
Deterministic Ordering Matters
Section titled “Deterministic Ordering Matters”Non-deterministic order makes the model hallucinate changes.
Sorting by normalized forward-slash paths is a simple and robust choice.
Pagination Without Stable Cursor Is Painful
Section titled “Pagination Without Stable Cursor Is Painful”Codex uses numeric 1-indexed offset. That works but can be brittle if directory contents change between calls.
A cursor-based design with stable ordering plus a “snapshot id” avoids this class of drift.
OpenOxide Blueprint
Section titled “OpenOxide Blueprint”Tool Contract
Section titled “Tool Contract”Expose list_dir as a first-class tool with both human-readable output and structured payload.
Inputs:
dir_path: AbsolutePathdepth: u32(default small; hard max)offset: u32(1-indexed) orcursor: Stringlimit: u32(bounded max)include_hidden: bool(default false)follow_symlinks: bool(default false)ignore: IgnorePolicy(built-in defaults + user config)
Outputs:
entries[]: { relative_path, kind, depth }truncated: boolnext_offsetornext_cursorsnapshot_id(optional)
Keep a compact, stable text rendering for humans, but do not force the model to parse prose.
Execution Model
Section titled “Execution Model”Implementation pipeline:
- validate that
dir_pathis inside the sandbox root - walk directory with bounded recursion
- do not traverse symlinks by default
- normalize paths to
/for output and sorting - sort globally by normalized path string
- slice by offset/limit
Ensure the walker is cancellation-aware (token from turn cancellation).
Safety and Policy
Section titled “Safety and Policy”- separate permission policy for metadata-only listing from file-content reads
- support allowlist/denylist patterns for list roots
- avoid listing protected internal directories by default (
.git/, tool state dirs)
Performance Strategy
Section titled “Performance Strategy”- short-circuit traversal when possible (if only shallow depth is requested)
- consider caching “directory snapshots” per turn to support repeated pagination
- keep per-entry metadata minimal (size/mtime optional behind flags)
Testing Strategy
Section titled “Testing Strategy”Unit tests:
- formatting: indentation, suffix markers
- sorting determinism across platforms
- depth semantics
- truncation metadata
- symlink loop safety
Integration tests:
- list under sandbox constraints
- permission-deny behavior and error surfaces
- large tree performance with enforced limit
Crates
Section titled “Crates”walkdirorignorefor traversaltokiofor async + cancellationserde/schemarsfor schema- OpenOxide permission/session crates
Key Design Decisions
Section titled “Key Design Decisions”- make list semantics explicit (filesystem vs VCS inventory)
- make truncation and paging explicit and machine-readable
- keep default ignore policy configurable