How PicList's Scripting System Works: Custom JavaScript Hooks for Image Upload Workflows

PicList's scripting system allows users to execute custom JavaScript code at specific lifecycle stages (such as before upload, after upload, or when the application opens) using a secure sandbox powered by Node.js's vm module, with access to core APIs like axios, fs, and environment variables.

The PicList scripting system in the kuingsmile/piclist repository enables power users to extend the application without modifying core source code. By placing JavaScript files in designated stage folders, you can hook into critical moments of the image upload workflow and manipulate data programmatically. This architecture separates user customizations from the main application logic while maintaining full access to PicList's internal context and Node.js utilities.

Architecture and Lifecycle Stages

The scripting engine operates through a structured pipeline that resolves scripts by lifecycle stage, filters enabled configurations, and executes code within a controlled sandbox environment.

Directory Structure and Stage Resolution

Scripts reside in a physical directory structure organized by lifecycle hooks. The scriptsDir() function in src/main/utils/runScript.ts returns the root scripts folder, which contains subdirectories named after specific stages:

  • scripts/onSoftwareOpen/ – Executes when PicList launches
  • scripts/beforeUpload/ – Runs prior to image upload initiation
  • scripts/upload/ – Executes during the upload process
  • scripts/afterUpload/ – Runs after successful uploads complete
  • Special stages like uploader.advancedplist for advanced plugin hooks

The scriptLifecycleStages array (lines 28-41 of src/main/utils/runScript.ts) enumerates all supported hook names, allowing plugin developers to register custom stages beyond the defaults.

The Execution Pipeline

When PicList triggers a lifecycle event, the runScriptInStage function (lines 103-139 of src/main/utils/runScript.ts) orchestrates the following flow:

  1. Path Resolution: Constructs the absolute path <scriptsRoot>/<stage> with special handling for advanced plugin stages
  2. Filtering: Skips scripts listed in config.scripts.disabledList as defined in src/renderer/utils/configPaths.ts
  3. Environment Preparation: The getFreshEnv() function (lines 14-24) parses .env files using dotenv and injects variables into process.env
  4. Sandbox Creation: Uses Node's vm module to create an isolated context with controlled global access
  5. Script Evaluation: Executes the raw JavaScript within the sandbox context
  6. Entry Point Invocation: Calls the user-defined main(ctx, extra) async function and awaits its return value

Sandbox Environment and Exposed APIs

The runScript function (lines 54-96 of src/main/utils/runScript.ts) creates a restricted execution context using vm.createContext(). This sandbox exposes specific PicList and Node.js utilities while blocking unsafe operations.

Available Global Objects

Each script receives access to:

  • ctx – The PicList core context providing logging via ctx.log.info(), ctx.log.error(), and configuration access
  • extra – Stage-specific data (e.g., the current upload payload or file metadata)
  • env – Parsed environment variables from the .env file loaded by getFreshEnv()
  • console – Wrapped to forward output to PicList's internal logger
  • Node.js modules: axios, crypto, fs, path, os, Buffer, and timing functions
  • base64Encode and base64Decode – Helper utilities for data transformation

The sandbox prevents access to the parent process or file system beyond the exposed APIs, ensuring that custom scripts cannot compromise application stability or access sensitive system resources outside the designated scope.

Writing Custom Scripts for PicList

Scripts must export a main function that accepts ctx and extra parameters. The function can be synchronous or asynchronous (returning a Promise).

Modifying Upload Payloads

Create a file at scripts/beforeUpload/modifyPayload.js to intercept and transform upload data:

function main(ctx, extra) {
  // Log to PicList's output panel
  ctx.log.info('Processing upload with custom script')
  
  // Access upload payload via extra
  const originalName = extra.fileName
  
  // Modify the payload for downstream processing
  extra.payload = {
    ...extra.payload,
    customTimestamp: Date.now(),
    originalIdentifier: originalName
  }
  
  // Return modified extra object to pass changes forward
  return extra
}

Using Environment Variables

Place a .env file in your scripts root directory, then access variables in scripts/onSoftwareOpen/loadEnv.js:

function main(ctx) {
  // Variables from .env are available on the global env object
  const apiKey = env.API_KEY
  const endpoint = env.CUSTOM_ENDPOINT
  
  if (!apiKey) {
    ctx.log.warn('API_KEY not configured in .env file')
    return
  }
  
  ctx.log.info(`Configured endpoint: ${endpoint}`)
  ctx.setConfig('customEndpoint', endpoint)
}

Asynchronous Operations

Scripts support async/await for network requests or file operations:

async function main(ctx, extra) {
  try {
    // Use the bundled axios instance
    const response = await axios.get('https://api.example.com/validate', {
      params: { filename: extra.fileName }
    })
    
    if (!response.data.allowed) {
      ctx.log.error('Upload blocked by validation service')
      throw new Error('Validation failed')
    }
    
    // Simulate processing delay
    await new Promise(resolve => setTimeout(resolve, 500))
    
    return extra
  } catch (error) {
    ctx.log.error(`Script execution failed: ${error.message}`)
    throw error
  }
}

Configuration and Script Management

Users control script execution through PicList's configuration interface and Vue-based management UI.

Disabling Scripts and Security

The configuration object picgo.getConfig().scripts (defined in src/renderer/utils/configPaths.ts) contains:

  • disabledList – An array of script paths (relative to the scripts directory) that the engine should ignore during stage execution
  • githubToken – Optional credentials for accessing the script marketplace

The ScriptPage.vue component in src/renderer/pages/ provides the graphical interface for toggling scripts on/off and editing code directly within the application.

Error Handling Behavior

When a script throws an exception or returns a rejected Promise, the error propagates through ctx.log.error and re-throws to the caller. This allows the host stage to decide whether to abort the current operation (such as canceling an upload) or continue execution. The sandbox ensures that script crashes do not destabilize the main PicList process.

Summary

  • Sandboxed Execution: PicList uses Node.js's vm module to run scripts in isolated contexts with controlled access to axios, fs, crypto, and other utilities via src/main/utils/runScript.ts.
  • Lifecycle Hooks: Scripts organize into stage-specific folders (beforeUpload, onSoftwareOpen, etc.) enumerated in scriptLifecycleStages, with runScriptInStage dispatching execution at appropriate times.
  • Data Flow: Scripts receive ctx (core PicList context) and extra (stage-specific data) parameters, modify objects in-place, and return values to influence downstream processing.
  • Environment Support: The getFreshEnv() function automatically loads .env files into the sandbox's env global variable, enabling secure credential management without hardcoding secrets.
  • Management Interface: Users enable, disable, and edit scripts through ScriptPage.vue, with persistence handled via picgo.getConfig().scripts.disabledList.

Frequently Asked Questions

What lifecycle stages are available in PicList?

PicList supports onSoftwareOpen, beforeUpload, upload, and afterUpload stages by default, with additional extensible stages like uploader.advancedplist available for plugin developers. The complete list is maintained in the scriptLifecycleStages array within src/main/utils/runScript.ts. Each stage corresponds to a subfolder in the scripts directory, and scripts placed in these folders execute automatically when that lifecycle event triggers.

How do I access environment variables in a PicList script?

Create a .env file in your root scripts directory (alongside the stage folders). When runScriptInStage executes, the getFreshEnv() function parses this file using dotenv and injects the variables into process.env while also exposing them as a global env object within the sandbox. Access variables directly via env.VARIABLE_NAME without requiring any import statements.

Is PicList's scripting system secure?

Yes, the system employs multiple security layers. The vm module creates an isolated context that prevents scripts from accessing the parent process or unrestricted file system. Only explicitly exposed APIs (such as axios, fs, ctx, and console) are available within the sandbox. Additionally, the disabledList configuration allows administrators to permanently prevent specific scripts from executing, and errors within scripts bubble up without crashing the main application.

Where does PicList store custom scripts on disk?

Scripts reside in the directory returned by the scriptsDir() function, typically located in the application's user data folder under a scripts subdirectory. The engine organizes scripts into stage-named subfolders (e.g., scripts/beforeUpload/). You can view and modify the current scripts directory path through src/main/utils/runScript.ts, and manage individual script files via the Script Page interface in src/renderer/pages/ScriptPage.vue.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →