> ## Documentation Index
> Fetch the complete documentation index at: https://docs.antigen.sh/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Pentest after a deployment

This example adds a pentest to your production deployment workflow. You’ll start with a script that runs tCell against your application, then connect it to GitHub Actions and limit testing to once every 24 hours.

The check runs after deployment. Critical vulnerabilities fail the check so your team can investigate.

The sections below explain each part. The [complete files](#complete-files) at the end combine them into a script and workflow you can copy into your repository.

## 1. Start a pentest

First, choose the production hostname you want to test and [submit it for approval](/docs/sdk/targets). This example checks for an exact hostname, such as `api.example.com`. Submit that hostname separately if you already have a wildcard target.

Install the SDK and `tsx`, which runs the TypeScript script:

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
npm install @antigen/sdk
npm install --save-dev tsx
```

The script reads your [API key](/docs/sdk/authentication) and target from environment variables. Before launching an agent, it checks that both are present and the target is approved:

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import Antigen, { tCell } from "@antigen/sdk";

const apiKey = process.env.ANTIGEN_API_KEY;
const target = process.env.PENTEST_TARGET;

if (!apiKey || !target) {
  throw new Error("Set ANTIGEN_API_KEY and PENTEST_TARGET before running.");
}

const antigen = new Antigen(apiKey);
const approved = await antigen.getApprovedTargets();

if (!approved.includes(target)) {
  throw new Error(`${target} is not approved for testing.`);
}
```

Retrieve tCell and give it a task containing the approved target:

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const agent = await antigen.agents.get(tCell);

const run = await agent.run({
  instructions: "Test the production application for vulnerabilities.",
  targets: [target],
});

console.log(`Started run ${run.id}`);
```

At this point, testing has started in a [sandbox](/docs/getting-started/concepts#agents-and-runs). The Run object lets your script follow the work and retrieve its results.

## 2. Give the agent more context

You can customize tCell before starting the run. For this example, ask it to investigate access between accounts and give it constraints for testing production.

Replace the agent lookup above with this configuration:

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const original = await antigen.agents.get(tCell);

const agent = antigen.agent({
  ...original,
  skills: [
    ...original.skills,
    `
      Investigate authorization across accounts.
      Check whether one account can read or modify
      resources belonging to another account.
    `,
  ],
  guardrails: [
    original.guardrails,
    "Do not attempt denial of service or delete customer data.",
  ].join("\n"),
});
```

The spreads preserve tCell’s configuration and existing skills. The additional skill describes what to investigate, while the guardrail constrains how the agent should work. Use these fields for your application’s own procedures and constraints. See [Skills](/docs/sdk/skills) and [Guardrails](/docs/sdk/guardrails) for more examples.

The call to `agent.run()` stays the same.

## 3. Follow the run and collect results

A pentest can take time. Iterate over the run to print activity as it happens, so someone viewing the CI logs can follow its progress:

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
for await (const event of run) {
  console.log(`${event.type}: ${event.summary}`);
}

await run.wait();
```

`run.wait()` resolves when testing completes and throws if the run fails or stops. Once it resolves, retrieve the vulnerabilities:

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const vulnerabilities = await run.vulnerabilities.list();
```

You now have the vulnerabilities from the completed run. The next step is deciding how they affect the CI check.

## 4. Make the results a CI check

This example fails the check when it finds a critical vulnerability. It saves all vulnerabilities to a JSON file so your team can review the results, including issues below that threshold.

Add the filesystem import at the top of the script, then save the results after retrieving them:

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
import { mkdir, writeFile } from "node:fs/promises";

await mkdir("pentest-results", { recursive: true });

await writeFile(
  "pentest-results/vulnerabilities.json",
  JSON.stringify(vulnerabilities, null, 2),
);
```

Set a nonzero exit code when a critical vulnerability is present:

```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
const critical = vulnerabilities.filter(
  (vulnerability) => vulnerability.severity === "critical",
);

if (critical.length > 0) {
  console.error(`Found ${critical.length} critical vulnerabilities.`);
  process.exitCode = 1;
}
```

The complete script wraps this work in `main()` and handles errors separately:

| Exit code | Meaning |
| - | - |
| `0` | Testing completed without critical vulnerabilities. |
| `1` | Testing completed and found critical vulnerabilities. |
| `2` | The script could not complete testing or retrieve the results. |

It also saves `run.json` as soon as the run starts. That file contains the run ID, target, and triggering deployment’s commit, which you can use to find the run later.

## 5. Run after a production deployment

With the script in place, connect it to GitHub Actions. Add these values under your repository’s **Settings → Secrets and variables → Actions**:

| Name | Kind | Value |
| - | - | - |
| `ANTIGEN_API_KEY` | Secret | Your Antigen API key. |
| `PENTEST_TARGET` | Variable | The approved production hostname. |

The workflow listens for deployment status updates and starts its job when the deployment succeeds in `production`:

```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
on:
  deployment_status:

jobs:
  pentest:
    if: >-
      github.event.deployment_status.state == 'success' &&
      github.event.deployment.environment == 'production'
    runs-on: ubuntu-latest
```

Your deployment provider must report status through GitHub’s [deployment status API](https://docs.github.com/en/rest/deployments/statuses). Change `production` if it uses a different environment name.

After checking out your default branch and installing dependencies, the workflow passes the configured values to the script:

```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
- name: Run pentest
  env:
    ANTIGEN_API_KEY: ${{ secrets.ANTIGEN_API_KEY }}
    PENTEST_TARGET: ${{ vars.PENTEST_TARGET }}
    DEPLOYMENT_SHA: ${{ github.event.deployment.sha }}
  run: npx --no-install tsx scripts/pentest.ts
```

The final step uploads `pentest-results/` as an artifact, including when critical vulnerabilities fail the check. Your team can download it from the workflow execution or review the vulnerabilities and evidence in Antigen.

If deployment itself runs in GitHub Actions, you can instead add the pentest job after your deployment job with `needs: deploy`. Adapt the condition to that workflow and pass the deployed commit as `DEPLOYMENT_SHA`. Events created using a workflow’s `GITHUB_TOKEN` generally do not trigger another workflow. See [Triggering a workflow](https://docs.github.com/en/actions/how-tos/writing-workflows/choosing-when-your-workflow-runs/triggering-a-workflow).

## 6. Limit testing to once every 24 hours

Your team may deploy several times a day. Before installing dependencies or starting a pentest, the workflow checks whether its **Run pentest** step has already started in the last 24 hours.

GitHub keeps that history, so the check can read earlier executions and their job steps. It includes previous attempts of rerun workflows. Once it finds a recent pentest attempt, it returns early:

```js theme={"theme":{"light":"github-light","dark":"github-dark"}}
if (recentAttempt) {
  core.info("Skipping: a pentest was attempted in the last 24 hours.");
  return false;
}
```

The check runs in a step named `cooldown`. Later steps use its result to decide whether to proceed:

```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
if: steps.cooldown.outputs.result == 'true'
```

A skipped step does not extend the window. For example, if testing starts at 10:00 on Monday, deployments later that day skip testing. The first deployment more than 24 hours after that attempt can start another pentest. Failed attempts count too, so repeated deployments do not repeatedly launch testing after an error.

A concurrency group makes subsequent jobs wait for the current job to finish. The next job checks the history before deciding whether to test:

```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
concurrency:
  group: antigen-production-pentest
  cancel-in-progress: false
```

This example shares one cooldown across the workflow’s production target. Keep the workflow, job, and pentest step names stable, and retain their history. If the history request fails, the job fails before testing starts.

## Complete files

Save these files in your repository and commit them with `package.json` and `package-lock.json` to your default branch.

<AccordionGroup>
  <Accordion title="scripts/pentest.ts">
    ```ts theme={"theme":{"light":"github-light","dark":"github-dark"}}
    import Antigen, { tCell } from "@antigen/sdk";
    import { mkdir, writeFile } from "node:fs/promises";

    async function main() {
      const apiKey = process.env.ANTIGEN_API_KEY;
      const target = process.env.PENTEST_TARGET;

      if (!apiKey || !target) {
        throw new Error(
          "Set ANTIGEN_API_KEY and PENTEST_TARGET before running.",
        );
      }

      const antigen = new Antigen(apiKey);
      const approved = await antigen.getApprovedTargets();

      if (!approved.includes(target)) {
        throw new Error(`${target} is not approved for testing.`);
      }

      const original = await antigen.agents.get(tCell);

      const agent = antigen.agent({
        ...original,
        skills: [
          ...original.skills,
          `
            Investigate authorization across accounts.
            Check whether one account can read or modify
            resources belonging to another account.
          `,
        ],
        guardrails: [
          original.guardrails,
          "Do not attempt denial of service or delete customer data.",
        ].join("\n"),
      });

      await mkdir("pentest-results", { recursive: true });

      const run = await agent.run({
        instructions: "Test the production application for vulnerabilities.",
        targets: [target],
      });

      console.log(`Started run ${run.id}`);

      await writeFile(
        "pentest-results/run.json",
        JSON.stringify(
          {
            runId: run.id,
            target,
            deploymentSha: process.env.DEPLOYMENT_SHA,
          },
          null,
          2,
        ),
      );

      for await (const event of run) {
        console.log(`${event.type}: ${event.summary}`);
      }

      await run.wait();

      const vulnerabilities = await run.vulnerabilities.list();

      await writeFile(
        "pentest-results/vulnerabilities.json",
        JSON.stringify(vulnerabilities, null, 2),
      );

      const critical = vulnerabilities.filter(
        (vulnerability) => vulnerability.severity === "critical",
      );

      if (critical.length > 0) {
        console.error(`Found ${critical.length} critical vulnerabilities.`);
        process.exitCode = 1;
        return;
      }

      console.log("Pentest completed without critical vulnerabilities.");
    }

    main().catch((error) => {
      console.error(error instanceof Error ? error.message : error);
      process.exitCode = 2;
    });
    ```
  </Accordion>

  <Accordion title=".github/workflows/pentest.yml">
    ```yaml theme={"theme":{"light":"github-light","dark":"github-dark"}}
    name: Production pentest

    on:
      deployment_status:

    permissions:
      contents: read
      actions: read

    jobs:
      pentest:
        name: Pentest production
        if: >-
          github.event.deployment_status.state == 'success' &&
          github.event.deployment.environment == 'production'
        runs-on: ubuntu-latest
        timeout-minutes: 350

        concurrency:
          group: antigen-production-pentest
          cancel-in-progress: false

        steps:
          - name: Check the last pentest attempt
            id: cooldown
            uses: actions/github-script@v9
            with:
              script: |
                const cutoff = Date.now() - 24 * 60 * 60 * 1000;

                const { data: current } =
                  await github.rest.actions.getWorkflowRun({
                    ...context.repo,
                    run_id: context.runId,
                  });

                const runs = await github.paginate(
                  github.rest.actions.listWorkflowRuns,
                  {
                    ...context.repo,
                    workflow_id: current.workflow_id,
                    per_page: 100,
                  },
                );

                for (const run of runs) {
                  if (
                    run.status === "completed" &&
                    Date.parse(run.updated_at) < cutoff
                  ) {
                    continue;
                  }

                  const jobs = await github.paginate(
                    github.rest.actions.listJobsForWorkflowRun,
                    {
                      ...context.repo,
                      run_id: run.id,
                      filter: "all",
                      per_page: 100,
                    },
                  );

                  const recentAttempt = jobs.some((job) =>
                    job.name === "Pentest production" &&
                    job.steps?.some((step) =>
                      step.name === "Run pentest" &&
                      step.status !== "queued" &&
                      step.conclusion !== "skipped" &&
                      step.started_at &&
                      Date.parse(step.started_at) >= cutoff
                    )
                  );

                  if (recentAttempt) {
                    core.info(
                      "Skipping: a pentest was attempted in the last 24 hours."
                    );
                    return false;
                  }
                }

                return true;

          - uses: actions/checkout@v7
            if: steps.cooldown.outputs.result == 'true'
            with:
              ref: ${{ github.event.repository.default_branch }}

          - uses: actions/setup-node@v7
            if: steps.cooldown.outputs.result == 'true'
            with:
              node-version: 24
              cache: npm

          - name: Install dependencies
            if: steps.cooldown.outputs.result == 'true'
            run: npm ci

          - name: Run pentest
            if: steps.cooldown.outputs.result == 'true'
            env:
              ANTIGEN_API_KEY: ${{ secrets.ANTIGEN_API_KEY }}
              PENTEST_TARGET: ${{ vars.PENTEST_TARGET }}
              DEPLOYMENT_SHA: ${{ github.event.deployment.sha }}
            run: npx --no-install tsx scripts/pentest.ts

          - name: Save results
            if: always() && steps.cooldown.outputs.result == 'true'
            uses: actions/upload-artifact@v7
            with:
              name: pentest-results-${{ github.run_id }}-${{ github.run_attempt }}
              path: pentest-results/
              if-no-files-found: ignore
    ```
  </Accordion>
</AccordionGroup>

Testing runs against the live application, which may change during the pentest. The deployment SHA in `run.json` identifies what triggered testing.

If CI is cancelled or times out, the agent can continue working. Use the run ID in the logs or artifact to [retrieve or stop it](/docs/sdk/stopping-and-resuming).
