Skip to content

cap_files — filesystem operations

Source: cap_files.c · header: cap_files.h

cap_files is the reference filesystem capability: it exposes full read/write access to a managed FATFS directory to the LLM and automation rules, and shows how to wrap filesystem access as tools safely on embedded targets.

The default managed root is /fatfs; change it at init with cap_files_set_base_dir().

Tool IDPurposeInput
read_fileRead a text filepath
write_fileCreate or overwrite a filepath, content
edit_fileReplace the first matching substringpath, old_string, new_string
delete_fileDelete a filepath
list_dirRecursively list files under the treeoptional prefix

All tools are flagged CLAW_CAP_FLAG_CALLABLE_BY_LLM.

The core guard is strict boundary checks to block path traversal:

static bool cap_files_path_is_valid(const char *path)
{
    // Disallow ".." (stay under base_dir)
    if (strstr(path, "..") != NULL) return false;

    // Path must start with base_dir
    if (strncmp(path, s_files_base_dir, base_len) != 0) return false;

    // Allow exact base_dir or base_dir/… children
    return path[base_len] == '\0' || path[base_len] == '/';
}

cap_files_resolve_path accepts absolute and relative inputs:

// Absolute: validate under base_dir
// "/fatfs/notes/hello.txt" → OK

// Relative: join under base_dir
// "notes/hello.txt" → "/fatfs/notes/hello.txt"
snprintf(resolved, resolved_size, "%s/%s", s_files_base_dir, path);

This lets the LLM use short relative paths while keeping a hard sandbox.

Reads are capped at 32 KB (CAP_FILES_MAX_FILE_SIZE); extra bytes are truncated so huge files cannot blow the LLM context:

max_read = output_size - 1;
if (max_read > CAP_FILES_MAX_FILE_SIZE) {
    max_read = CAP_FILES_MAX_FILE_SIZE;
}
read_size = fread(output, 1, max_read, file);
output[read_size] = '\0';

write_file recursively creates missing parents—no manual mkdir:

cap_files_ensure_parent_dirs(resolved_path);
// e.g. /fatfs → /fatfs/notes → /fatfs/notes/2026 …

edit_file does one substring replace (strstr + memcpy), not global replace:

match = strstr(buffer, old_string);
if (!match) {
    snprintf(output, output_size, "Error: old_string not found in %s", resolved_path);
    return ESP_ERR_NOT_FOUND;
}
// prefix + new_string + suffix, write back once

That gives precise edits and clear errors when old_string is wrong.

// Walk base_dir recursively, emit full paths per file
// Optional prefix filter (only matching paths)
cap_files_list_recursive(s_files_base_dir, prefix, output, output_size, &offset, &count);

Output is one absolute path per line:

/fatfs/notes/hello.txt
/fatfs/scripts/blink.lua
/fatfs/skills/cap_lua_run.md

Empty trees return "(no files found)" instead of a blank string so the LLM does not misread silence.

cap_files and cap_lua manage different subtrees:

ModuleTreeFile types
cap_files/fatfs (configurable)Any text file
cap_lua/fatfs/scripts (default).lua only

The LLM can edit Lua with cap_files and run with cap_lua, or read .md skills managed by cap_skill.

// Change base dir (before register_group)
cap_files_set_base_dir("/sdcard");

// Register with claw_cap
cap_files_register_group();