Camper: running security campaigns on Temporal

AUTHORS
Kent Gruber
PUBLISHED
Oct 01, 2026
DURATION
12 MIN
  • Code Samples
  • Security
  • Temporal Primitives

When the same security problem appears across repositories, fixing the code is only one part of the work. We also need to find the full scope, reach the right owners, get useful changes through CI and review, handle the places where the pattern does not fit, and know when we are actually done.

At Temporal, we organize that kind of work as a security campaign. A campaign is a focused effort around one class of risk, such as pinning GitHub Actions to immutable commits, adding dependency cooldowns, upgrading a vulnerable library, or changing a pattern across repositories and teams. It can cover a single repository or a much larger part of the organization. Every repository keeps its normal engineering workflow: tickets, pull requests, CI runs, reviews, the works. The campaign gives us a way to understand and orchestrate the work across them.

A campaign is done when every item in scope has an answer. Sometimes that answer is a merged fix. Other times, it is a documented exception, an archived repository, or a proposed change that a maintainer correctly rejected.

Our first campaigns ran with fairly little automation. Jira knew about the tickets. GitHub knew about the pull requests. Our scanners knew about the findings. Slack helped me find reviewers. I was the piece gluing them together.

This mostly worked, in the sense that I could spend a lot of time making it work. But there were limits, and a lot of clicking.

After we finished our first campaign, I started building Camper. The first version connected the tools we already used and automated parts of the process. It imported findings, created tickets, generated candidate changes, and opened pull requests in batches. That saved time, but a running campaign still had too much state spread across tools and people's heads.

Moving Camper onto Temporal Workflows changed that. A campaign could now wait for review, easily retry a failed API call, accept new instructions, and resume after a restart without asking an operator to reconstruct what had happened.

Camper now runs real security campaigns on Temporal. It has also run its first campaign outside Security, a cross-repository credential migration in Release Engineering, and that is where we learned the most about what a second operator actually needs.

The campaign is the unit of work#

A generated pull request is useful, but it does not tell us whether the campaign is finished. Camper tracks the longer operation around it.

A campaign starts with a specification that describes the objective, scope, integrations, approval mode, and remediation strategy. Camper creates a Campaign Workflow and a Finding Workflow for each discovered finding. Each Finding Workflow can create a Jira ticket, invoke a remediator, open a pull request, wait for CI and review, and record the final outcome.

The change itself can come from different places: Deputy handles supported, rule-driven transformations. Codex can propose a change when repository context matters. Some findings should stop at a ticket so a person can do the work. Camper records the chosen path and keeps the finding visible until an operator records its disposition.

This shape matters because campaigns run on human time. A pull request may sit for a day or a week. A reviewer may ask for a change. The base branch may move. An external API may fail. An operator may learn that another repository belongs in scope.

Temporal gives each of those events a durable place to land. When a Workflow is waiting, it is not a cron job repeatedly guessing what happened. When an Activity fails, Camper can retry it according to policy. When a maintainer or campaign owner sends new information, the running Workflow can continue from its recorded state. Operators can inspect that state and event history in the Temporal UI.

Whichever path a finding takes, Camper keeps its scope, state, and review loop connected.

What a campaign looks like#

Here is a compact campaign specification for pinning third-party GitHub Actions to immutable commits. It uses manual approval, Jira for tracked work, GitHub for review, and Deputy for the known transformation.

name: "Pin third-party GitHub Actions"
summary: >-
  Replace mutable action tags with immutable commit SHAs while preserving
  exact version comments for review.

description: |
  Repository maintainers review every pull request and decide what merges.
  Leave local actions and temporalio/* actions unchanged.

automation_mode: AUTOMATION_MODE_MANUAL_APPROVAL

finding_source:
  plugin: manual
  config:
    rules:
      - id: unpinned-github-actions
        title: "Mutable GitHub Actions reference"
        description: "A workflow uses a mutable action tag such as @v4."
        remediation_guidance: >-
          Pin third-party actions to full commit SHAs and retain the resolved
          version in a comment.

targets:
  - repository:
      slug: temporalio/reference-app-orders-web
      url: https://github.com/temporalio/reference-app-orders-web
      ref: main

issue_tracker:
  plugin: jira

source_control:
  plugin: github
  config:
    ref: main

remediator:
  plugin: deputy
  config:
    mode: pin
    ecosystems:
      - github-actions
    exclude:
      - temporalio/*
    network_access: true
    skip_verification: false
    pr_context_title: "Why this change"
    pr_context_body: >-
      This pull request is part of a bounded campaign to replace mutable
      third-party action tags. Repository maintainers retain merge authority.

max_remediation_attempts: 3

pr_workflow_config:
  auto_merge: false
  require_approval: true
  max_fix_attempts: 3

This example produces one finding for the target repository. The same shape works with one target or many, and the owner can add targets while an active campaign is running. Jira project settings and credentials come from the Camper deployment, so they are not embedded in the file.

network_access is a Camper policy guard. Deputy needs network access for modes that resolve remote references or fetch package metadata. Camper requires the operator to opt in before invoking those modes. It does not turn Deputy into a sandbox, and it does not replace CI or review.

The operator can inspect the specification before starting anything:

camper campaign validate pin-github-actions.yaml
camper campaign plan pin-github-actions.yaml
camper campaign apply pin-github-actions.yaml

CAMPAIGN_ID="..."
camper campaign watch "$CAMPAIGN_ID"

validate checks the schema and a small set of local consistency rules. It does not verify that a plugin is enabled, validate every plugin-specific setting, or prove that credentials and connected services work. plan asks the Camper server for a rough estimate from the declared targets. It does not run discovery or predict the eventual finding count. Neither command creates Workflows, tickets, or pull requests. apply starts the durable campaign.

Because this campaign uses manual approval, Camper creates the tracked work and waits. The operator starts remediation for a finding after confirming its scope and owner:

FINDING_ID="..."

camper finding remediate "$CAMPAIGN_ID" "$FINDING_ID" \
  --reason "scope and repository owner confirmed"

Campaigns also change while they run. If review teaches us something that should apply to unfinished findings, the owner can update the specification and propagate the new guidance. If another repository enters scope, the owner can add it without rebuilding the campaign.

camper campaign update "$CAMPAIGN_ID" pin-github-actions.yaml --propagate
camper campaign add-target "$CAMPAIGN_ID" owner/repository

Propagation reaches only non-terminal findings, and a per-finding override still wins, so finished work stays untouched. That lets us improve a campaign while it is running without pretending we knew everything at the start.

Choosing the right way to make the change#

There is no single remediation tool that fits every campaign.

For GitHub Actions pinning, Camper can call Deputy's deterministic pin operation. Deputy resolves each supported reference to an immutable commit and produces a predictable diff. We recently made Deputy open source, including the policy and verification work around those transformations.

Other campaigns need more repository context. Camper can give Codex the finding, campaign instructions, repository contents, and relevant review feedback, then treat the result as a candidate change. By default, the built-in Codex remediator confines writes to its cloned workspace and keeps network access disabled. A campaign has to broaden those permissions explicitly when the work requires it.

Sometimes neither option is appropriate. Camper can create the ticket and wait while the team routes it to an owner and a person writes the fix.

I do not want campaign automation to hide these choices. The specification records the selected remediator, its campaign-scoped configuration, attempt limits, and approval mode. Deployment credentials and worker policy define its actual authority. The most predictable tool that fits the work is usually the right one.

Review stays with maintainers#

Opening a pull request does not mean remediation is completed. It is the point where the proposed change meets the repository's CI, ownership, and local context.

Camper keeps that normal GitHub workflow intact. It watches checks and formal review state. When a maintainer with write access leaves actionable inline review comments, Camper can feed those comments into a bounded fix cycle on the existing pull request. Maintainers with write access can also steer the running Workflow from the pull request:

/camper retry
/camper rebase
/camper resolve-conflict
/camper close

/camper rebase asks GitHub to update a conflict-free branch. /camper retry and /camper resolve-conflict regenerate against the current base and update the existing pull-request branch. /camper close closes the proposal and records who requested it.

There is deliberately no /camper merge comment command. In the manual campaigns described here, the repository's normal merge path remains authoritative.

Review also catches cases where the campaign guidance is wrong. In one pull request, an automated review flagged that the proposed read-only credentials would break steps that needed write access, so the campaign owner closed it with /camper close.

As of September 29, 2026, Camper had opened at least 150 public pull requests across 72 temporalio repositories, and 111 of them had merged. Private repositories are not included.

The SDK teams have often been especially quick to help. api-go#300 merged 2 minutes and 20 seconds after Camper opened it. sdk-rust#1275 merged in 12 minutes and 34 seconds. Those are examples, not a benchmark. They show how little coordination a narrow change can require when the context is clear, CI is useful, and the right maintainer is available.

The first campaign outside Security#

An engineer in Release Engineering gave Camper its first serious use outside Security. They were working on a cross-repository credential migration and had already used Codex to help build tooling around it. Their question was fair: if an agent can write a migration script, what is Camper adding?

The answer showed up in the work around the script. The campaign still needed a defined scope, owners, tracked decisions, pull requests, review follow-up, retries, and a record of what merged or did not. Code generation did not remove any of that.

They started Camper locally, connected it to a Temporal Cloud namespace, and ran a real campaign. They found the sort of gaps that a demo does not: Jira permissions, where non-Security campaigns should live, how much context a pull request needs, and what another operator needs when something goes wrong. They also improved Camper while using it.

That mattered to me. This was not another polished demo of something Security already knew how to operate. Someone else was using Camper to get their own work done, finding sharp edges, and making the project better in the process.

Security is still Camper's first and deepest use case. That campaign showed that the same model can help with other cross-repository changes too.

From one local process to Temporal Cloud#

I did not want trying Camper to turn into an infrastructure project of its own.

For local development, one command starts Camper's API, a Temporal worker, and an embedded Temporal server in the same process:

camper server dev --noop

--noop replaces the external integrations with safe local stand-ins. It lets a developer exercise the API, Workflow wiring, and campaign lifecycle without Docker or accounts for Jira, GitHub, or OpenAI. The default is disposable. Adding --persist --data-dir ./.camper-dev keeps the embedded Temporal history and Camper's application state together so the work can resume after a restart.

The same Camper binary can use the same campaign specification with self-hosted Temporal or Temporal Cloud. A small Temporal Cloud setup looks like this:

export TEMPORAL_ADDRESS="example.account.tmprl.cloud:7233"
export TEMPORAL_NAMESPACE="example.account"
export TEMPORAL_API_KEY="..."
export CAMPER_ENABLE_JIRA=true
export CAMPER_ENABLE_GITHUB=true
export CAMPER_ENABLE_DEPUTY=true

camper server \
  --enable-temporal \
  --temporal-task-queue camper-campaigns \
  --embedded-worker \
  --storage sqlite \
  --data-dir ./.camper-cloud \
  --listen 127.0.0.1:8080

The integration credentials are omitted here, but the enablement is intentional. Plain camper server does not turn Jira, GitHub, Deputy, or the other real plugins on automatically.

Moving from a laptop to Temporal Cloud does not introduce a second campaign model. The Workflow code, Task Queue contract, retries, Signals, and campaign specification remain the same. Larger deployments can separate API and worker processes and use PostgreSQL for shared Camper state.

Where we go from here#

Camper is private and early. We plan to open source it, much as we did with Deputy, but there is work to do before that is responsible and useful.

Another operator should be able to deploy Camper, understand what it is doing, recover it when an integration fails, and know which credentials it holds. We are working on the shared service shape, operational visibility, runbooks, and credential boundaries. We also want more operators inside Security to be able to run Camper confidently.

I started Camper because I got tired of carrying campaign state in my head. It is already carrying some of that load for us. Now we are working to make it something more teams can operate confidently and eventually use in the open.

Temporal Cloud

Ready to see for yourself?

Sign up for Temporal Cloud today and get $150 in free credits.

Build invincible applications

It sounds like magic, we promise it's not.