Wp filesystem api
Skill Lonsdale201/wp-agent-skills/wordpress/wp-filesystem-api
Read, write, copy, delete, chmod files from a WordPress plugin via the `WP_Filesystem` abstraction instead of bare PHP. Covers the bootstrap (`require_once ABSPATH . 'wp-admin/includes/file.php'` → `request_filesystem_credentials()` → `WP_Filesystem()` → `$wp_filesystem->*` methods), 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.From its SKILL.md
npx -y skills add Lonsdale201/wp-agent-skills --skill wp-filesystem-apiAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 21 stars21 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
14.5 KB, ~3.4k tokens by cl100k_base, as published. Nobody here has run it
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're about to write to" — wp-admin/includes/file.php:2364. 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 at wp-admin/includes/file.php:2364-2393:
| 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() (file.php:2260) 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 set on first successful WP_Filesystem() call (file.php:2208-2228):
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.
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() (wp-admin/includes/file.php:1097) |
| 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.
Pattern: write generated CSS into uploads on settings save
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'] ) ) {
return;
}
if ( ! wp_mkdir_p( $dir ) ) {
return;
}
if ( false === file_put_contents( $file, $css, LOCK_EX ) ) {
return;
}
}, 10, 2 );
This is intentionally plain PHP for the normal local uploads filesystem; 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. Use WP_Filesystem when writing outside
uploads, where an admin page can show the credentials form.
Pattern: read a JSON config the plugin ships
For READING bundled assets, just use PHP — no credentials needed:
$config = json_decode( file_get_contents( MYPLUGIN_DIR . '/config/defaults.json' ), true );
WP_Filesystem for reads is unnecessary indirection. The abstraction exists for WRITES on hosts where the web user lacks privileges.
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 AI mistakes
// WRONG — fails on hosts where web user != file owner
file_put_contents( WP_CONTENT_DIR . '/myplugin-cache.json', $data );
// RIGHT — on an admin page, request creds, then initialize WP_Filesystem
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;
}
// WRONG — credentials form leaks into REST / AJAX response
add_action( 'rest_api_init', static function (): void {
register_rest_route( 'myplugin/v1', '/clear-cache', array(
'callback' => static function () {
request_filesystem_credentials( '' ); // echoes <form>; corrupts JSON response
// ...
}
) );
} );
// RIGHT — only request creds in admin page contexts; gate by transport availability elsewhere
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.' );
}
Cross-references
- See
wp-plugin-options-storagefor storing config — most "I need to write a file" needs are better served by an option / transient. - See
wp-security-deepfor 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
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/
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.