agentsclimarketplace

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

Install
npx -y skills add Lonsdale201/wp-agent-skills --skill wp-filesystem-api

Assembled 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 / 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

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

ReturnMeaningWhat you do
trueNo credentials needed — direct available on this hostProceed to WP_Filesystem()
arrayUser entered FTP/SSH credentialsPass to WP_Filesystem( $creds )
falseForm was rendered AND no submission yetreturn — 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:

PriorityMethodCondition
1directPHP can write as the same owner as WP files (or $allow_relaxed_file_ownership set + dir already writable)
2ssh2PHP ssh2 extension loaded AND user picked SSH in the form
3ftpextPHP ftp extension loaded
4ftpsocketsPHP 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 = 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):

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

NeedRight 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_uploadPlain 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_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 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-storage for storing config — most "I need to write a file" needs are better served by an option / transient.
  • See wp-security-deep for 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

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 325,949. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.