# Install-Time Lifecycle Script Introduction and Content Diff (`kingii98/install-time-lifecycle-script-introduction-and-content-diff`) Actor

Compare the current and the proposed version of a dependency, and report every install-time script that the new version adds, removes or changes, with the flagged lines and a clean, review or block verdict.

- **URL**: https://apify.com/kingii98/install-time-lifecycle-script-introduction-and-content-diff.md
- **Developed by:** [kingii98](https://apify.com/kingii98) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $15.00 / 1,000 run\_starteds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Install-Time Lifecycle Script Introduction and Content Diff Gate

Answer one question before you merge a dependency-bump pull request: **does the
new version run code at install time that the old version did not run, and what
does that code do?**

For each version pair, the Actor gets the old artifact and the new artifact from
the public registry, reads the install-time scripts of both, and reports every
script that the new version **adds, removes or changes**. A long-standing
install script is not news, so only a difference is reported. Each added or
changed script is scanned line by line for the shapes that an install-time
attack needs, and the pair gets a `clean`, `review` or `block` verdict.

The Actor keeps no state between runs. It reads the old version from the
registry inside the same run, so you keep no baseline.

### What the Actor reads

| Ecosystem | Artifact | Install-time surface |
| --- | --- | --- |
| `npm` | the published `.tgz` | the `preinstall`, `install`, `postinstall`, `prepare` and `prepublish` commands in `package.json`, plus the local file each command names (for example `install.js`), plus an implicit `node-gyp rebuild` when the package holds `binding.gyp` and declares no install script |
| `pypi` | the source distribution | `setup.py`, the file pip runs when it builds an sdist. A version that publishes wheels only runs no install-time code, and the Actor records that without a download |

A script counts as **changed** when the command text changes **or** when the
content of the file it names changes. That content diff is the point: `node
install.js` can stay the same while `install.js` becomes something else.

### Input

| Field | Type | Default | Meaning |
| --- | --- | --- | --- |
| `bumps` | list of strings | four public example pairs | 1 to 200 version pairs, each `ecosystem:name@from_version..to_version`, for example `npm:left-pad@1.1.3..1.3.0`, `npm:@scope/pkg@1.0.0..2.0.0` or `pypi:psutil@5.9.7..5.9.8`. An object with `package`, `from_version` and `to_version` is accepted too |
| `flag_patterns` | list | `[]` | Extra texts to flag inside an added or changed script. Each text is a literal, case-insensitive substring, not a regular expression. Write `{"name": "our_domain", "text": "x.test", "severity": "block"}` to make one a block |
| `max_artifact_mb` | integer | `25` | The Actor skips a larger artifact, records the reason, and does not bill the pair |

Every field has a schema default, so a run with empty input `{}` succeeds and
reports the four example pairs.

#### The built-in flag patterns

| Pattern | Severity | What it finds |
| --- | --- | --- |
| `shell_pipe_exec` | block | downloaded text piped into a shell or an interpreter |
| `encoded_payload` | block | a base64 or hex payload that the script decodes |
| `dynamic_eval` | block | code built at run time and evaluated |
| `write_outside_package` | block | a write outside the package directory, for example to the home directory or to cron |
| `credential_access` | block | a read of `.npmrc`, `.ssh`, AWS credentials or a token in the environment |
| `network_fetch` | review | a network call while the package installs |
| `charcode_assembly` | review | text built out of character codes |
| `process_spawn` | review | another process started while the package installs |

A line that only holds a comment runs nothing, so the scan steps over it.

### Verdicts

| Verdict | When |
| --- | --- |
| `clean` | the new version adds, removes and changes no install-time script |
| `review` | a script is added, removed or changed, and nothing matches a block pattern; also a pair that could not be read, was skipped for size, or was written incorrectly |
| `block` | an added or changed script matches a block-severity pattern |

A business verdict is never a failed run. A blocked bump, an unreachable
registry, a malformed pair and a run with zero findings all end as a **succeeded**
run that carries the verdict in the dataset and in the run status message.

### Output

One dataset row for each version pair, plus one run-summary record.

| Row field | Contract field | Meaning |
| --- | --- | --- |
| `package`, `fromVersion`, `toVersion` | package, from\_version, to\_version | what was compared |
| `scriptsBefore`, `scriptsAfter` | scripts\_before, scripts\_after | every install-time script of each version, with its name, its command text, the file it names and that file's sha256 |
| `scriptsAdded`, `scriptsRemoved`, `scriptsChanged` | scripts\_added, scripts\_removed, scripts\_changed | the difference, with the command and the file digest of both sides |
| `flaggedPatterns` | flagged\_patterns | the pattern name, the severity, the file, the line number and the matched line |
| `artifactDigestBefore`, `artifactDigestAfter` | artifact\_digest\_before, artifact\_digest\_after | the sha256 of the two artifacts the Actor read |
| `verdict`, `blockReason` | verdict, block\_reason | `clean`, `review` or `block`, and the sentence that carries it |

The summary record holds `runVerdict`, `pairsRequested`, `pairsInspected`, the
verdict counts, the script counts, `flaggedScriptCount`, `invalidCount` and
`unreadableCount`.

### Billing (pay per event)

| Event | Unit | Counted |
| --- | --- | --- |
| `run_started` | one run | once for each run, before the work starts |
| `version_pair_inspected` | one version pair | once for each unique version pair whose two artifacts the Actor read. A repeated pair is charged once. A malformed, skipped or unreachable pair is never charged |
| `flagged_script_extracted` | one flagged script | once for each added or changed install-time script whose text matched a flag pattern. A clean bump list pays nothing for this event |

### Bounds and safety

- HTTP only. No browser, no proxy, no key, no secret and no external database.
- Three fixed public hosts: `registry.npmjs.org`, `pypi.org` and
  `files.pythonhosted.org`. A buyer string never becomes a host, a redirect is
  followed by hand at most twice, and only to a host in that list, so a private
  or reserved target cannot be reached.
- 200 version pairs for each run, 4 downloads at a time, a 30 s timeout for each
  call, 25 MB for each artifact (64 MB limit), 1 MB for each file read out of an
  archive, and at most 12 files read out of one archive.
- The Actor never unpacks a whole artifact. It reads the manifest and the few
  files the install scripts name. An absolute path, a parent-directory path and
  a member that declares more than the file limit are all refused.
- A buyer-supplied flag pattern is a literal substring, so no pattern can make
  the scan slow.

### Local use

```bash
uv sync
uv run pytest
uv run ruff check .
apify run            # uses .actor/default_input.json
```

# Actor input Schema

## `bumps` (type: `array`):

1 to 200 dependency bumps, each written as `ecosystem:name@from_version..to_version`. The ecosystem is `npm` or `pypi`, for example `npm:left-pad@1.1.3..1.3.0`, `npm:@scope/pkg@1.0.0..2.0.0` or `pypi:psutil@5.9.7..5.9.8`. An object with `package`, `from_version` and `to_version` is accepted too. A malformed pair becomes a row with a reason, and does not stop the run.

## `flag_patterns` (type: `array`):

Optional list of extra texts to flag inside an added or changed install-time script. Each text is matched as a literal, case-insensitive substring, so no pattern can make the scan slow. The built-in patterns already cover a network fetch, a shell pipe into an interpreter, an encoded payload, a write outside the package directory, dynamic evaluation and credential access. An extra pattern raises a `review`; write it as an object with `"severity": "block"` to make it a block.

## `max_artifact_mb` (type: `integer`):

The Actor skips an artifact larger than this limit and records the reason on the row. A skipped pair is reported as `review` and is not billed.

## Actor input object example

```json
{
  "bumps": [
    "npm:left-pad@1.1.3..1.3.0",
    "npm:esbuild@0.20.2..0.21.5",
    "pypi:psutil@5.9.7..5.9.8",
    "pypi:idna@3.6..3.7"
  ],
  "flag_patterns": [],
  "max_artifact_mb": 25
}
```

# Actor output Schema

## `dataset` (type: `string`):

One row for each version pair, plus one run-summary record that carries the run verdict and the counts.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "bumps": [
        "npm:left-pad@1.1.3..1.3.0",
        "npm:esbuild@0.20.2..0.21.5",
        "pypi:psutil@5.9.7..5.9.8",
        "pypi:idna@3.6..3.7"
    ],
    "flag_patterns": [],
    "max_artifact_mb": 25
};

// Run the Actor and wait for it to finish
const run = await client.actor("kingii98/install-time-lifecycle-script-introduction-and-content-diff").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "bumps": [
        "npm:left-pad@1.1.3..1.3.0",
        "npm:esbuild@0.20.2..0.21.5",
        "pypi:psutil@5.9.7..5.9.8",
        "pypi:idna@3.6..3.7",
    ],
    "flag_patterns": [],
    "max_artifact_mb": 25,
}

# Run the Actor and wait for it to finish
run = client.actor("kingii98/install-time-lifecycle-script-introduction-and-content-diff").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "bumps": [
    "npm:left-pad@1.1.3..1.3.0",
    "npm:esbuild@0.20.2..0.21.5",
    "pypi:psutil@5.9.7..5.9.8",
    "pypi:idna@3.6..3.7"
  ],
  "flag_patterns": [],
  "max_artifact_mb": 25
}' |
apify call kingii98/install-time-lifecycle-script-introduction-and-content-diff --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,kingii98/install-time-lifecycle-script-introduction-and-content-diff"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/MUo54gyKEZhAiEaWY/builds/PMVn0JuL9lAPKuy4w/openapi.json
