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

> Discover how PicList's scripting system uses secure Node.js sandboxing for custom JavaScript hooks in image upload workflows. Enhance your uploads with powerful lifecycle automation.

- Repository: [Kuingsmile/piclist](https://github.com/kuingsmile/piclist)
- Tags: internals
- Published: 2026-03-05

---

**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`](https://github.com/kuingsmile/piclist/blob/main/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`](https://github.com/kuingsmile/piclist/blob/main/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`](https://github.com/kuingsmile/piclist/blob/main/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`](https://github.com/kuingsmile/piclist/blob/main/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`](https://github.com/kuingsmile/piclist/blob/main/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`](https://github.com/kuingsmile/piclist/blob/main/scripts/beforeUpload/modifyPayload.js) to intercept and transform upload data:

```javascript
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`](https://github.com/kuingsmile/piclist/blob/main/scripts/onSoftwareOpen/loadEnv.js):

```javascript
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:

```javascript
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`](https://github.com/kuingsmile/piclist/blob/main/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`](https://github.com/kuingsmile/piclist/blob/main/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`](https://github.com/kuingsmile/piclist/blob/main/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`](https://github.com/kuingsmile/piclist/blob/main/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`](https://github.com/kuingsmile/piclist/blob/main/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`](https://github.com/kuingsmile/piclist/blob/main/src/main/utils/runScript.ts), and manage individual script files via the Script Page interface in [`src/renderer/pages/ScriptPage.vue`](https://github.com/kuingsmile/piclist/blob/main/src/renderer/pages/ScriptPage.vue).