wp-filesystem-api
Read, write, copy, delete, chmod files from a WordPress
Install
npx skills add https://github.com/Lonsdale201/wp-agent-skills/tree/main/wordpress/wp-filesystem-api
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install lonsdale201-wp-agent-skills@llmmart
git clone https://github.com/Lonsdale201/wp-agent-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole lonsdale201/wp-agent-skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
WordPress Filesystem API
WP abstracts filesystem access because on shared hosts the web user often can't write plugin/theme/core paths directly — FTP/SSH credentials are required. WP_Filesystem picks the right transport (direct when PHP can write as the file owner, otherwise ssh2 / ftpext / ftpsockets) and exposes a uniform method set. Plugins that call file_put_contents() directly outside writable uploads fail on those hosts; plugins that use WP_Filesystem work across more hosting setups.
When to use this skill
Trigger when ANY of the following is true:
- A plugin needs to write files outside
wp-content/uploads/— generated CSS, bundled asset extraction, log files, exported reports, cache, mu-plugins install. - Code references
WP_Filesystem,request_filesystem_credentials,get_filesystem_method,FS_METHOD,FS_CHMOD_FILE,FS_CHMOD_DIR,$wp_filesystem,WP_Filesystem_Direct,WP_Filesystem_SSH2,WP_Filesystem_FTPext,WP_Filesystem_ftpsockets. - Code uses
file_put_contents/fwrite/fopen/unlink/mkdir/rmdirfor plugin-owned files outside uploads. - The user reports: "works on my localhost, fails on the shared host", "the file doesn't write", "users see an FTP prompt out of nowhere".
The bootstrap — five lines, in this exact order
// 1. Load the API. NOT autoloaded.
require_once ABSPATH . 'wp-admin/includes/file.php';
// 2. Request creds (returns true for direct, an array for FTP/SSH, false if form was shown).
$creds = request_filesystem_credentials( $form_url, '', false, $context );
if ( false === $creds ) {
return; // Form was rendered; wait for the next request.
}
// 3. Initialize (returns true|false|null).
if ( ! WP_Filesystem( $creds, $context ) ) {
// Bad creds — re-render the form with an error.
request_filesystem_credentials( $form_url, '', true, $context );
return;
}
// 4. Use the global instance.
global $wp_filesystem;
$remote_context = $wp_filesystem->find_folder( $context );
if ( false === $remote_context ) {
return;
}
if ( ! $wp_filesystem->put_contents(
trailingslashit( $remote_context ) . 'generated.css',
$css,
FS_CHMOD_FILE
) ) {
return;
}
The $context arg is a directory you are about to write to. It controls writability detection (get_filesystem_method writes a temp file there to confirm permissions) and determines whether direct is safe.
$context is a local WordPress path used for method detection. Filesystem
methods need paths in the selected transport's namespace. With direct they
look identical; with FTP/SSH they may not. Convert with find_folder() or use
base mappings such as $wp_filesystem->wp_content_dir(),
wp_plugins_dir(), or abspath() before appending a relative path.
What request_filesystem_credentials() returns
Verified in request_filesystem_credentials() in wp-admin/includes/file.php:
| Return | Meaning | What you do |
|---|---|---|
true |
No credentials needed — direct available on this host |
Proceed to WP_Filesystem() |
array |
User entered FTP/SSH credentials | Pass to WP_Filesystem( $creds ) |
false |
Form was rendered AND no submission yet | return — wait for the form post |
The function ALSO ECHOES the form when no creds are present and $_POST is empty. Don't render your own output before calling it on an admin POST handler — the form must reach the screen.
The four transports
get_filesystem_method() picks in this order:
| Priority | Method | Condition |
|---|---|---|
| 1 | direct |
PHP can write as the same owner as WP files (or $allow_relaxed_file_ownership set + dir already writable) |
| 2 | ssh2 |
PHP ssh2 extension loaded AND user picked SSH in the form |
| 3 | ftpext |
PHP ftp extension loaded |
| 4 | ftpsockets |
PHP sockets extension OR fsockopen() available |
Only the host operator should force a specific method in wp-config.php,
after checking ownership and write permissions:
define( 'FS_METHOD', 'direct' ); // Operator-controlled; never set by a plugin.
Defaults established by WP_Filesystem() initialization:
FS_CONNECT_TIMEOUT = 30FS_TIMEOUT = 30FS_CHMOD_DIR = (fileperms(ABSPATH) & 0777) | 0755FS_CHMOD_FILE = (fileperms(ABSPATH . 'index.php') & 0777) | 0644
Always pass FS_CHMOD_FILE / FS_CHMOD_DIR to write/mkdir operations so permissions match WP's own.
The methods you'll actually use
From WP_Filesystem_Base (wp-admin/includes/class-wp-filesystem-base.php):
global $wp_filesystem;
// Read.
$wp_filesystem->exists( $path ); // bool
$wp_filesystem->is_file( $path ); // bool
$wp_filesystem->is_dir( $path ); // bool
$wp_filesystem->is_readable( $path ); // bool
$wp_filesystem->is_writable( $path ); // bool
$wp_filesystem->size( $path ); // int bytes
$wp_filesystem->mtime( $path ); // int unix ts
$wp_filesystem->get_contents( $path ); // string|false
$wp_filesystem->get_contents_array( $path ); // array|false (one line per element)
$wp_filesystem->dirlist( $path, $hidden = true, $recursive = false );
// Write.
$wp_filesystem->put_contents( $path, $content, FS_CHMOD_FILE ); // bool
$wp_filesystem->touch( $path ); // bool
$wp_filesystem->mkdir( $path, FS_CHMOD_DIR ); // bool
$wp_filesystem->chmod( $path, $mode, $recursive = false ); // bool
$wp_filesystem->copy( $source, $dest, $overwrite = false, FS_CHMOD_FILE );
$wp_filesystem->move( $source, $dest, $overwrite = false );
// Delete.
$wp_filesystem->delete( $path, $recursive = false, $type = false ); // $type: 'f' | 'd' | false
$wp_filesystem->rmdir( $path, $recursive = false );
put_contents returns true on success — always check.
WordPress 7.1 transport hardening
WordPress 7.1 tightened the concrete filesystem transports. This mostly turns warnings, invalid coercions, and ambiguous results into explicit failure values, so callers that already check every result continue to work:
- path mapping helpers such as
abspath(),wp_content_dir(),wp_plugins_dir(), andwp_themes_dir()are explicitlystring|false; get_contents_array()returnsstring[]|false, including on disconnected FTP transports;- FTP/SSH constructors accept missing options defensively and connection calls stop on constructor configuration errors;
- disconnected FTP/SSH methods and failed directory listings return
falsemore consistently; - owner/group may be a name, a positive numeric ID, or
false; - recursive
chgrp()/chown()in the direct transport now traverse the actual listing names, and defaultchmod()preserves the detected mode instead of forcibly OR-ing file bits.
Do not branch on transport-specific warning behavior. Check false from path,
read, listing, stat, and mutation methods on every supported WordPress version.
Do not instantiate WP_Filesystem_FTPext, WP_Filesystem_ftpsockets, or
WP_Filesystem_SSH2 directly; let WP_Filesystem() validate and connect them.
When NOT to use WP_Filesystem
| Need | Right tool |
|---|---|
Writing a user-uploaded file to wp-content/uploads/ |
wp_handle_upload() or media_handle_upload() |
| Reading any existing file (where you don't need write capability) | Plain PHP file_get_contents() is fine — read access doesn't need FS abstraction |
Touching a local file that you just got back from wp_handle_upload |
Plain PHP, after checking every return value; storage-offload plugins may change the lifecycle |
Anything in PHP tempnam() / sys_get_temp_dir() |
Plain PHP |
WP_Filesystem is overkill for wp-content/uploads. It's the right tool for wp-content/, wp-content/plugins/<self>/cache/, wp-content/mu-plugins/, or anywhere ELSE that needs the credentials dance on restricted hosts.
Usage patterns
Read references/patterns-and-mistakes.md for checked examples covering local
uploads, bundled-file reads, admin credential flows, and REST-safe failure.
Critical rules
require_once ABSPATH . 'wp-admin/includes/file.php'before any of the API. Not autoloaded. Frontend, REST, and cron contexts don't include it.- Always pass
FS_CHMOD_FILE/FS_CHMOD_DIRtoput_contents/mkdir/copy. Skipping them means permissions are left to whatever the transport's defaults are — often too-open on direct, too-locked on FTP. - Pass the same
$contexttorequest_filesystem_credentials()andWP_Filesystem(). CallingWP_Filesystem()with no context defaults detection toWP_CONTENT_DIR, which can be wrong when you're targeting a deeper plugin/cache directory. request_filesystem_credentials()ECHOES a form when no creds are stored. Don't call it from a page that's already streamed HTML, and don't call it from REST / AJAX — it's not designed for those contexts.- The credentials prompt is admin-interactive, not the filesystem object itself. Cron, REST, and CLI cannot render that form. They may initialize
WP_Filesystem()whendirectis available or credentials were supplied through trusted host configuration; otherwise fail cleanly and require an admin setup step. - Don't reach for
WP_Filesystemto write insidewp-content/uploads—wp_handle_uploador plain PHP suffices. Reserve it for paths that might NOT be web-user-writable. $wp_filesystem->delete( $path )does NOT recurse by default. Pass$recursive = truewhen deleting non-empty directories, and pass$type('f'or'd') when the type is known — saves a stat call.- Never prescribe
FS_METHOD = 'direct'as a generic plugin fix. It is a host-operator override after ownership and permissions are verified. Forcing it on a mismatched host can fail or create files with ownership that later blocks core updates. Never define it from a plugin. - Don't store FTP credentials. WP intentionally does NOT persist the password — the user re-enters it each session. Don't add your own "save FTP password" UI.
Common mistakes
Read references/patterns-and-mistakes.md before moving a filesystem mutation
into REST, AJAX, cron, or CLI; the credentials form is admin-interactive.
Cross-references
- See
wp-plugin-options-storagefor storing config — most "I need to write a file" needs are better served by an option / transient. - Do your own path traversal checks (
realpath(),wp_normalize_path()) —WP_Filesystemdoesn't validate paths for you. - See
wp-plugin-cronwhen an FS-touching task moves to background — note the'direct'requirement.
What this skill does NOT cover
- WP's update / upgrader APIs (
WP_Upgrader,Plugin_Upgrader,Theme_Upgrader) which useWP_Filesysteminternally. Different abstraction layer for the install/update flow. - WP-CLI's
\WP_CLI\Utils\http_requestfor downloading files. WP-CLI commands typically use plain PHP for FS — see thewp-cli-extendingskill. - The
wp-content/uploads/upload pipeline (wp_handle_upload,media_handle_upload,wp_handle_sideload). Adjacent topic — different bootstrap.
References
- Detailed patterns and failure cases:
references/patterns-and-mistakes.md. wp-admin/includes/file.php:2169—WP_Filesystem()initializer (sets$wp_filesystemglobal, defines FS_CHMOD_* constants).wp-admin/includes/file.php:2260—get_filesystem_method()with the transport priority and writability detection.wp-admin/includes/file.php:2364—request_filesystem_credentials()(returnstrue|false|array, echoes form when needed).wp-admin/includes/class-wp-filesystem-base.php— the base class method surface (lines 487-861 cover the public methods listed above).wp-admin/includes/file.php:1097—wp_handle_upload()for the uploads/ pipeline.- Official documentation: https://developer.wordpress.org/reference/classes/wp_filesystem_base/
- Official documentation: https://developer.wordpress.org/reference/functions/wp_filesystem/
- Official documentation: https://developer.wordpress.org/reference/functions/request_filesystem_credentials/
- Official documentation: https://developer.wordpress.org/advanced-administration/wordpress/wp-config/
Files (wp-agent-skills)
-
references
-
patterns-and-mistakes.md 3 KB
# WP Filesystem patterns and mistakes ## Write generated CSS into uploads The ordinary uploads directory normally uses its own pipeline rather than the interactive credentials abstraction: ```php add_action( 'update_option_myplugin_options', static function ( $old, $new ): void { if ( $old === $new ) { return; } $upload = wp_upload_dir(); $dir = $upload['basedir'] . '/myplugin'; $file = $dir . '/style.css'; $css = ':root { --brand: ' . sanitize_hex_color( $new['brand_color'] ?? '#000' ) . '; }'; if ( ! empty( $upload['error'] ) || ! wp_mkdir_p( $dir ) ) { return; } if ( false === file_put_contents( $file, $css, LOCK_EX ) ) { return; } }, 10, 2 ); ``` Every directory/write result is checked. Object-storage or stream-wrapper plugins can alter the upload lifecycle, so do not assume a permanent local file after third-party offload hooks run. ## Read a bundled JSON file Reading a file shipped inside the plugin does not need write credentials: ```php $json = file_get_contents( MYPLUGIN_DIR . '/config/defaults.json' ); if ( false === $json ) { return new WP_Error( 'myplugin_config_read', 'Config is not readable.' ); } $config = json_decode( $json, true, 32, JSON_THROW_ON_ERROR ); ``` Use a compatible non-throwing JSON error path when the plugin supports PHP versions where that design is not appropriate. ## Admin-interactive write outside uploads ```php require_once ABSPATH . 'wp-admin/includes/file.php'; $form_url = wp_nonce_url( admin_url( 'admin.php?page=myplugin' ), 'myplugin_write_cache' ); $context = WP_CONTENT_DIR; $creds = request_filesystem_credentials( $form_url, '', false, $context ); if ( false === $creds ) { return; } if ( ! WP_Filesystem( $creds, $context ) ) { request_filesystem_credentials( $form_url, '', true, $context ); return; } global $wp_filesystem; $remote_content = $wp_filesystem->wp_content_dir(); if ( ! $remote_content || ! $wp_filesystem->put_contents( trailingslashit( $remote_content ) . 'myplugin-cache.json', $data, FS_CHMOD_FILE ) ) { return; } ``` Do not replace this with `file_put_contents( WP_CONTENT_DIR . ... )`; that fails where the web user and file owner differ. ## REST, AJAX, cron, and CLI Never call `request_filesystem_credentials()` in a REST/AJAX response or a background process. It can echo an HTML form and cannot complete interactively. In a non-interactive context, either use already-provisioned trusted host credentials, require the direct method, or fail cleanly: ```php require_once ABSPATH . 'wp-admin/includes/file.php'; if ( 'direct' !== get_filesystem_method() ) { return new WP_Error( 'fs_unavailable', 'Server requires admin filesystem access.' ); } if ( ! WP_Filesystem() ) { return new WP_Error( 'fs_unavailable', 'Filesystem initialization failed.' ); } ``` Do not turn `FS_METHOD=direct` into a plugin recommendation. Host ownership and permissions determine whether that override is safe.
-
-
SKILL.md 13.1 KB
--- name: wp-filesystem-api description: Read, write, copy, delete, chmod files from a WordPress plugin via the `WP_Filesystem` abstraction instead of bare PHP. Covers the bootstrap sequence from loading `wp-admin/includes/file.php` through `request_filesystem_credentials()` and `WP_Filesystem()` to filesystem method calls, the four transports (direct, ssh2, ftpext, ftpsockets) selected by `get_filesystem_method()`, the `FS_METHOD` / `FS_CHMOD_FILE` / `FS_CHMOD_DIR` constants, the credentials form flow, and when to use `wp_handle_upload()` / `wp_upload_dir()` instead. Use for plugin writes outside `wp-content/uploads`, generated CSS/cache files outside uploads, log output, bundled-asset extraction, and any FS op that must work on FTP-only shared hosts. metadata: wp-skills-author: "Soczó Kristóf" wp-skills-contact: "mailto:lonsdale201@hotmail.com" wp-skills-plugin: "wordpress" wp-skills-plugin-version-tested: "6.0 - 7.1" wp-skills-wp-version-tested: "7.1" wp-skills-php-min: "7.4" wp-skills-last-updated: "2026-08-20" --- # WordPress Filesystem API WP abstracts filesystem access because on shared hosts the web user often can't write plugin/theme/core paths directly — FTP/SSH credentials are required. `WP_Filesystem` picks the right transport (`direct` when PHP can write as the file owner, otherwise `ssh2` / `ftpext` / `ftpsockets`) and exposes a uniform method set. Plugins that call `file_put_contents()` directly outside writable uploads fail on those hosts; plugins that use `WP_Filesystem` work across more hosting setups. ## When to use this skill Trigger when ANY of the following is true: - A plugin needs to write files outside `wp-content/uploads/` — generated CSS, bundled asset extraction, log files, exported reports, cache, mu-plugins install. - Code references `WP_Filesystem`, `request_filesystem_credentials`, `get_filesystem_method`, `FS_METHOD`, `FS_CHMOD_FILE`, `FS_CHMOD_DIR`, `$wp_filesystem`, `WP_Filesystem_Direct`, `WP_Filesystem_SSH2`, `WP_Filesystem_FTPext`, `WP_Filesystem_ftpsockets`. - Code uses `file_put_contents` / `fwrite` / `fopen` / `unlink` / `mkdir` / `rmdir` for plugin-owned files outside uploads. - The user reports: "works on my localhost, fails on the shared host", "the file doesn't write", "users see an FTP prompt out of nowhere". ## The bootstrap — five lines, in this exact order ```php // 1. Load the API. NOT autoloaded. require_once ABSPATH . 'wp-admin/includes/file.php'; // 2. Request creds (returns true for direct, an array for FTP/SSH, false if form was shown). $creds = request_filesystem_credentials( $form_url, '', false, $context ); if ( false === $creds ) { return; // Form was rendered; wait for the next request. } // 3. Initialize (returns true|false|null). if ( ! WP_Filesystem( $creds, $context ) ) { // Bad creds — re-render the form with an error. request_filesystem_credentials( $form_url, '', true, $context ); return; } // 4. Use the global instance. global $wp_filesystem; $remote_context = $wp_filesystem->find_folder( $context ); if ( false === $remote_context ) { return; } if ( ! $wp_filesystem->put_contents( trailingslashit( $remote_context ) . 'generated.css', $css, FS_CHMOD_FILE ) ) { return; } ``` The `$context` arg is a directory you are about to write to. It controls writability detection (`get_filesystem_method` writes a temp file there to confirm permissions) and determines whether `direct` is safe. `$context` is a local WordPress path used for method detection. Filesystem methods need paths in the selected transport's namespace. With `direct` they look identical; with FTP/SSH they may not. Convert with `find_folder()` or use base mappings such as `$wp_filesystem->wp_content_dir()`, `wp_plugins_dir()`, or `abspath()` before appending a relative path. ## What `request_filesystem_credentials()` returns Verified in `request_filesystem_credentials()` in `wp-admin/includes/file.php`: | Return | Meaning | What you do | |---|---|---| | `true` | No credentials needed — `direct` available on this host | Proceed to `WP_Filesystem()` | | `array` | User entered FTP/SSH credentials | Pass to `WP_Filesystem( $creds )` | | `false` | Form was rendered AND no submission yet | `return` — wait for the form post | The function ALSO ECHOES the form when no creds are present and `$_POST` is empty. Don't render your own output before calling it on an admin POST handler — the form must reach the screen. ## The four transports `get_filesystem_method()` picks in this order: | Priority | Method | Condition | |---|---|---| | 1 | `direct` | PHP can write as the same owner as WP files (or `$allow_relaxed_file_ownership` set + dir already writable) | | 2 | `ssh2` | PHP `ssh2` extension loaded AND user picked SSH in the form | | 3 | `ftpext` | PHP `ftp` extension loaded | | 4 | `ftpsockets` | PHP `sockets` extension OR `fsockopen()` available | Only the host operator should force a specific method in `wp-config.php`, after checking ownership and write permissions: ```php define( 'FS_METHOD', 'direct' ); // Operator-controlled; never set by a plugin. ``` Defaults established by `WP_Filesystem()` initialization: - `FS_CONNECT_TIMEOUT = 30` - `FS_TIMEOUT = 30` - `FS_CHMOD_DIR = (fileperms(ABSPATH) & 0777) | 0755` - `FS_CHMOD_FILE = (fileperms(ABSPATH . 'index.php') & 0777) | 0644` Always pass `FS_CHMOD_FILE` / `FS_CHMOD_DIR` to write/mkdir operations so permissions match WP's own. ## The methods you'll actually use From `WP_Filesystem_Base` (`wp-admin/includes/class-wp-filesystem-base.php`): ```php global $wp_filesystem; // Read. $wp_filesystem->exists( $path ); // bool $wp_filesystem->is_file( $path ); // bool $wp_filesystem->is_dir( $path ); // bool $wp_filesystem->is_readable( $path ); // bool $wp_filesystem->is_writable( $path ); // bool $wp_filesystem->size( $path ); // int bytes $wp_filesystem->mtime( $path ); // int unix ts $wp_filesystem->get_contents( $path ); // string|false $wp_filesystem->get_contents_array( $path ); // array|false (one line per element) $wp_filesystem->dirlist( $path, $hidden = true, $recursive = false ); // Write. $wp_filesystem->put_contents( $path, $content, FS_CHMOD_FILE ); // bool $wp_filesystem->touch( $path ); // bool $wp_filesystem->mkdir( $path, FS_CHMOD_DIR ); // bool $wp_filesystem->chmod( $path, $mode, $recursive = false ); // bool $wp_filesystem->copy( $source, $dest, $overwrite = false, FS_CHMOD_FILE ); $wp_filesystem->move( $source, $dest, $overwrite = false ); // Delete. $wp_filesystem->delete( $path, $recursive = false, $type = false ); // $type: 'f' | 'd' | false $wp_filesystem->rmdir( $path, $recursive = false ); ``` `put_contents` returns `true` on success — always check. ## WordPress 7.1 transport hardening WordPress 7.1 tightened the concrete filesystem transports. This mostly turns warnings, invalid coercions, and ambiguous results into explicit failure values, so callers that already check every result continue to work: - path mapping helpers such as `abspath()`, `wp_content_dir()`, `wp_plugins_dir()`, and `wp_themes_dir()` are explicitly `string|false`; - `get_contents_array()` returns `string[]|false`, including on disconnected FTP transports; - FTP/SSH constructors accept missing options defensively and connection calls stop on constructor configuration errors; - disconnected FTP/SSH methods and failed directory listings return `false` more consistently; - owner/group may be a name, a positive numeric ID, or `false`; - recursive `chgrp()` / `chown()` in the direct transport now traverse the actual listing names, and default `chmod()` preserves the detected mode instead of forcibly OR-ing file bits. Do not branch on transport-specific warning behavior. Check `false` from path, read, listing, stat, and mutation methods on every supported WordPress version. Do not instantiate `WP_Filesystem_FTPext`, `WP_Filesystem_ftpsockets`, or `WP_Filesystem_SSH2` directly; let `WP_Filesystem()` validate and connect them. ## When NOT to use `WP_Filesystem` | Need | Right tool | |---|---| | Writing a user-uploaded file to `wp-content/uploads/` | `wp_handle_upload()` or `media_handle_upload()` | | Reading any existing file (where you don't need write capability) | Plain PHP `file_get_contents()` is fine — read access doesn't need FS abstraction | | Touching a local file that you just got back from `wp_handle_upload` | Plain PHP, after checking every return value; storage-offload plugins may change the lifecycle | | Anything in PHP `tempnam()` / `sys_get_temp_dir()` | Plain PHP | `WP_Filesystem` is overkill for `wp-content/uploads`. It's the right tool for `wp-content/`, `wp-content/plugins/<self>/cache/`, `wp-content/mu-plugins/`, or anywhere ELSE that needs the credentials dance on restricted hosts. ## Usage patterns Read `references/patterns-and-mistakes.md` for checked examples covering local uploads, bundled-file reads, admin credential flows, and REST-safe failure. ## Critical rules - **`require_once ABSPATH . 'wp-admin/includes/file.php'`** before any of the API. Not autoloaded. Frontend, REST, and cron contexts don't include it. - **Always pass `FS_CHMOD_FILE` / `FS_CHMOD_DIR`** to `put_contents` / `mkdir` / `copy`. Skipping them means permissions are left to whatever the transport's defaults are — often too-open on direct, too-locked on FTP. - **Pass the same `$context` to `request_filesystem_credentials()` and `WP_Filesystem()`**. Calling `WP_Filesystem()` with no context defaults detection to `WP_CONTENT_DIR`, which can be wrong when you're targeting a deeper plugin/cache directory. - **`request_filesystem_credentials()` ECHOES a form when no creds are stored**. Don't call it from a page that's already streamed HTML, and don't call it from REST / AJAX — it's not designed for those contexts. - **The credentials prompt is admin-interactive, not the filesystem object itself**. Cron, REST, and CLI cannot render that form. They may initialize `WP_Filesystem()` when `direct` is available or credentials were supplied through trusted host configuration; otherwise fail cleanly and require an admin setup step. - **Don't reach for `WP_Filesystem` to write inside `wp-content/uploads`** — `wp_handle_upload` or plain PHP suffices. Reserve it for paths that might NOT be web-user-writable. - **`$wp_filesystem->delete( $path )` does NOT recurse by default**. Pass `$recursive = true` when deleting non-empty directories, and pass `$type` (`'f'` or `'d'`) when the type is known — saves a stat call. - **Never prescribe `FS_METHOD = 'direct'` as a generic plugin fix**. It is a host-operator override after ownership and permissions are verified. Forcing it on a mismatched host can fail or create files with ownership that later blocks core updates. Never define it from a plugin. - **Don't store FTP credentials**. WP intentionally does NOT persist the password — the user re-enters it each session. Don't add your own "save FTP password" UI. ## Common mistakes Read `references/patterns-and-mistakes.md` before moving a filesystem mutation into REST, AJAX, cron, or CLI; the credentials form is admin-interactive. ## Cross-references - See **`wp-plugin-options-storage`** for storing config — most "I need to write a file" needs are better served by an option / transient. - Do your own path traversal checks (`realpath()`, `wp_normalize_path()`) — `WP_Filesystem` doesn't validate paths for you. - See **`wp-plugin-cron`** when an FS-touching task moves to background — note the `'direct'` requirement. ## What this skill does NOT cover - WP's update / upgrader APIs (`WP_Upgrader`, `Plugin_Upgrader`, `Theme_Upgrader`) which use `WP_Filesystem` internally. Different abstraction layer for the install/update flow. - WP-CLI's `\WP_CLI\Utils\http_request` for downloading files. WP-CLI commands typically use plain PHP for FS — see the `wp-cli-extending` skill. - The `wp-content/uploads/` upload pipeline (`wp_handle_upload`, `media_handle_upload`, `wp_handle_sideload`). Adjacent topic — different bootstrap. ## References - Detailed patterns and failure cases: `references/patterns-and-mistakes.md`. - `wp-admin/includes/file.php:2169` — `WP_Filesystem()` initializer (sets `$wp_filesystem` global, defines FS_CHMOD_* constants). - `wp-admin/includes/file.php:2260` — `get_filesystem_method()` with the transport priority and writability detection. - `wp-admin/includes/file.php:2364` — `request_filesystem_credentials()` (returns `true|false|array`, echoes form when needed). - `wp-admin/includes/class-wp-filesystem-base.php` — the base class method surface (lines 487-861 cover the public methods listed above). - `wp-admin/includes/file.php:1097` — `wp_handle_upload()` for the uploads/ pipeline. - Official documentation: <https://developer.wordpress.org/reference/classes/wp_filesystem_base/> - Official documentation: <https://developer.wordpress.org/reference/functions/wp_filesystem/> - Official documentation: <https://developer.wordpress.org/reference/functions/request_filesystem_credentials/> - Official documentation: <https://developer.wordpress.org/advanced-administration/wordpress/wp-config/>
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.