For AI agents: the complete documentation index is available at /llms.txt, the full documentation bundle is available at /llms-full.txt, and this page is available as Markdown at /automations/factory-on-public-repositories.md.

Best practices for Tessl Factory on public repositories

Use this guide before allowing Factory to work on a public GitHub repository, or before making an existing Factory repository public. You'll choose who can hand work to Factory, limit the access its agents receive, and require a person to approve code before merging.

Factory turns issues into pull requests and follows up on them using agents that read discussion, execute commands, and push code. Public issues, comments, and files can contain malicious instructions. The steps below reduce that risk, including when a trusted maintainer starts the work.

Before you begin

This guide starts after Factory has been connected to your repository. In Tessl, open the repository's project and select Loops. A Loop is one automation. Its triggers are the events that start it, such as an issue receiving a label.

The examples below use the standard Factory configuration created during onboarding. If you've changed your Factory, compare each step with your current Loops, triggers, schedules, and actions rather than copying the examples unchanged. Apply the same principles to any additional paths that can start work, and repeat these checks after changing your configuration.

You should see Implement ticket, Shepherd PR, and Follow ticket changes. Your setup may also include Merge conflict sweep, learning Loops, or copies of these Loops. If Factory hasn't been set up yet, ask your organization administrator to complete that first.

You'll need:

  • Access to edit the project's Loops and open and merge configuration PRs in GitHub.
  • A GitHub repository administrator to set branch rules and an organization administrator to review Tessl environments.
  • A local checkout of the repository, with the GitHub CLI and Tessl CLI installed and signed in.

If the repository doesn't expect outside contributions, you can temporarily limit participation to collaborators in GitHub Settings → Moderation options → Interaction limits. These limits expire after at most 6 months. They reduce incoming public activity but don't replace the steps below. GitHub interaction limits

1. Choose the maintainers who can request work

Implement ticket starts when someone applies the Factory label (tessl by default) to an issue. Opening a public issue alone doesn't start it.

  1. Choose the maintainers allowed to apply the Factory label. Review each issue before applying it. Disable Apps or Actions that automatically apply it to public submissions.

  2. In GitHub, give label access only to trusted maintainers. In organization repositories, the Triage role or higher can apply and remove labels. Editing label definitions requires Write. The Tessl GitHub App also needs to manage labels, through its installation permissions rather than a collaborator role. GitHub label permissions

  3. Get each chosen maintainer's numeric GitHub account ID and your repository's ID. In your terminal, replace the placeholders and run:

    gh api users/<login> --jq .id
    gh api repos/<owner>/<repo> --jq .id

Keep these numbers for the configuration changes below. Account IDs identify who may start follow-up work. The repository ID identifies which repository a PR's branch must belong to.

2. Open a PR containing the Loop settings

The settings need to live in your repository so you can change their trigger conditions. You're opening a configuration PR, not asking Factory to implement an issue.

On the project's Loops page, find Shepherd PR, Follow ticket changes, and any copies. Open each Loop and look at the button beside its name:

What you seeWhat to do
EditUse the Loop's action menu to Pause it and confirm its status is Paused. Click Edit, then Open pull request in the dialog. Open the resulting PR in GitHub.
View pull requestA move into tessl.json is already in progress. Open that PR and confirm the Loop is paused. Continue editing that PR rather than creating another.
Edit in tessl.jsonThe settings are already in the repository. Follow the link to the file, create a branch, and open a PR for your changes. The current settings stay live until your change is applied. Use Stop work first if they must stop immediately.

If none of these buttons appears, ask your project administrator to check your access and where the Loop's settings are managed. Don't create a replacement Loop to work around it.

The PR adds this Loop's settings to the project's tessl.json, creating the file if necessary. Edit that file on the PR's branch, in GitHub or your local checkout. It may be in a project subdirectory, so use the file linked by Tessl rather than creating another at the repository root.

Keep the generated action names unchanged. Tessl uses them to match the repository configuration to the existing Loop and retain its runs and findings. Renaming one can create a duplicate Loop.

Work through the next steps for each configuration PR. The web action moves one Loop at a time. If another tool has put several Loops into one PR, check every Loop included. Don't merge yet. Pausing protects this preparation period, but a moved Loop can become active when the merged configuration is applied.

3. Edit who can start follow-up work

In the configuration PR, open tessl.json. Under triggers, each entry has:

  • on: the GitHub event that can start work.
  • when: the conditions that event must meet.
  • action: the work to run, either written here or named in the actions block.

For Shepherd PR and Follow ticket changes, make the edits below. Keep existing label, branch, and other conditions, along with action and inputs. Check the conditions even if you used a setup wizard or CLI.

Allow only your chosen maintainers

In each trigger for the events in the table below, add this property inside its existing when object. If event.sender.id already exists, replace its value:

{
  "event.sender.id": { "in": [12345678, 87654321] }
}

Replace the example numbers with your maintainers' IDs. This example shows the property to add to your existing when object. Keep its other conditions. It isn't a complete tessl.json file.

Event in onOther changes inside when
github:issue_comment.createdAdd "event.issue.state": "open". Keep isPullRequest as "false" for issue replies and "true" for PR comments.
github:pull_request_review.submittedIn the human-review trigger, add the open-state and repository conditions below. Remove the automated-review trigger in the next subsection.
github:pull_request_review_comment.createdAdd the open-state and repository conditions below.
github:issues.unlabeledAdd the account-ID restriction only.
github:issues.closedAdd the account-ID restriction only.
github:pull_request.closedAdd the account-ID restriction and "event.pull_request.head.repo.id": 123456789, using your repository ID. Don't add an open-state condition to a closure event.

For human reviews and inline review comments, add these properties inside when:

{
  "event.pull_request.state": "open",
  "event.pull_request.head.repo.id": 123456789
}

Replace 123456789 with your repository ID. A PR from a contributor's fork has a different head-repository ID, so this condition rejects it at the review trigger.

PR-comment events don't provide that same head-repository check. A comment can start a run before the agent checks for a fork. Refusal before checking out fork code relies on the agent following its instructions, not on an enforced repository-ID boundary for that event.

Remove automatic CI and review follow-ups

In Shepherd PR settings, remove the entries whose on events are:

  • github:check_run.completed
  • github:check_suite.completed

Also remove the separate github:pull_request_review.submitted entry whose event.sender.id selects Tessl's Reviewer bot. Keep the human-review entry and restrict it as above.

Tests and code reviews will still run. A maintainer reads their results and leaves a comment or review when they want Factory to make a change. This prevents an outsider's request for an automated review from becoming an automatic request to change code.

Use Factory for PRs whose branches are in the connected repository. Handle contributions from forks outside Factory: review and update those PRs yourself. Don't ask Factory to make changes in a fork PR's comments, because a maintainer's comment can still start a run.

Keep other automatic work off for this setup

Check the Loops list for these additional paths:

LoopWhy to check itWhat to change
Merge conflict sweepA push to the default branch can make it start Shepherd PR to fix conflicts, without a maintainer comment or review. It uses permission to start another project action, rather than a maintainer's comment.Keep it paused in Tessl. If its settings are already in tessl.json, or included in your configuration PR, remove its starting entries from triggers and schedules.
Turn findings into ticketsLearning Loops can read public PR discussion and record Findings, which are recommendations. This Loop can turn those recommendations into new Factory work.Keep it paused in Tessl. If its settings are in the repository or included in the PR, remove its starting entries from triggers and schedules.

For a moved Loop, keep its generated action name so Tessl can match it to the original. Removing its event and scheduled starts prevents automatic work, but leaves the action available for direct invocation by an authorized caller. To stop that action entirely, follow Stop work. Review action-trigger permissions in the next step as well.

4. Protect code and credentials before enabling work

Ask a GitHub repository administrator to protect the default branch in Settings → Rules → Rulesets:

  1. Require a PR and passing checks.
  2. Require human approval, renewed after changes.
  3. Don't give agent accounts or Apps a bypass.

If your project publishes from tags, add an Active tag ruleset in Settings → Rules → Rulesets covering your release-tag pattern, such as v*. Enable Restrict creations, Restrict updates, and Restrict deletions. Give bypass access only to your release maintainers, excluding the Tessl GitHub App and agent accounts. Default-branch rules alone don't protect this tag-triggered publishing path. GitHub tag rules

To require a human reviewer, add .github/CODEOWNERS on the default branch and enable Require review from Code Owners. Replace this example with a visible human-only team with write access:

* @your-org/maintainers

This covers every file, including CODEOWNERS, tessl.json, workflows, and release configuration. Check that later CODEOWNERS entries don't override it. Workflow and release edits can change publishing behavior even when Factory's environment has no publishing credentials. GitHub CODEOWNERS

Tests and setup scripts run before merge and can read available credentials. Human merge approval doesn't protect those credentials. Sandboxing and scanning can't prevent every credential leak.

In each Loop's configuration, find the environment it uses. Ask a Tessl organization administrator to review those environments in Organization settings → Environments:

  • Start with a dedicated, GitHub-only environment containing the access Factory needs, bound only to projects that need it. Comments, commits, and PR descriptions can become public immediately. Before adding another credential, have an administrator review why it's needed and how it could be exposed. Keep production secrets, signing keys, publishing credentials, and broad personal tokens out.
  • Keep GitHub access: write for unchanged standard Factory skills. They need it for issue labels and CI operations. Lowering it requires changing that behavior. Both write and pull-requests can merge where GitHub protections allow it, which is why the branch rules above matter.
  • Leave Linear access at none unless needed. Disable reading other runs' logs unless a Loop needs that access. Remove unnecessary custom tokens and MCP connections, checking the environment, Loop settings, and legacy workspace settings.
  • If available, review Project memory (projectMemory: none, read, write) and Action trigger (actionTrigger: none, project). Both default to none. Grant them only to Loops that need to read or propose project facts, or start other declared project actions. Action-trigger calls are a separate path from the event filters you edited.

See Least privilege for environments for the access rules.

In .github/workflows/, don't run untrusted PR code in privileged pull_request_target or workflow_run jobs. GitHub Actions security

Store publishing and other sensitive Actions credentials as GitHub environment secrets, rather than repository or organization secrets available to these workflows. In GitHub Settings → Environments, require human reviewers and select the allowed branches and tags. Every job using those credentials must reference that environment. Review the commit and workflow before approving the job. This gates the referencing jobs and their environment secrets. GitHub environments

5. Merge the configuration PR and verify Tessl applied it

Before merging, review the PR changing tessl.json. Check the account and repository IDs, removed automatic starts, and unchanged action names, inputs, and unrelated conditions. Include every Loop and copy in the PR.

Merge it to the default branch. Tessl automatically reads the merged file and updates the Loops. There's no separate import step. A paused Loop being moved into the repository can become active as soon as that configuration is applied, so the restrictions must be in the PR before merge.

Return to Loops and open each changed Loop:

  1. Confirm it shows Defined in tessl.json and Synced from commit with the merged configuration commit, or a later commit containing those changes. Follow the commit link to check the file if unsure.
  2. Open Configuration. Expand its trigger rows and compare their Conditions with your edits: maintainer IDs, open-state checks, and repository IDs where applicable.
  3. Confirm the removed CI/reviewer events aren't listed. For automatic Loops you're keeping off, check how each is managed: Tessl-managed Loops must show Paused. Repository-managed Loops must have no starting entries in either triggers or schedules.
  4. Check the Loops list for duplicate copies with the old rules.

If it still says Moving to tessl.json, shows an older source commit, or the list reports Last sync failed, click Sync loops on the project's Loops page. Wait for the result and repeat the checks. If synchronization fails, fix the reported problem in a configuration PR and sync again. Don't treat a merged PR alone as proof that the new settings are active.

Repeat this process for any remaining Loops. Remove account IDs from the configuration when maintainers leave.

6. Check the setup and assign someone to monitor it

Use a disposable repository without sensitive credentials to check the behavior before relying on it:

  • A public issue alone doesn't start work. A maintainer can start it by applying the Factory label.
  • Only your chosen accounts can request comment/review follow-ups. Tests and automated reviews don't request code changes on their own.
  • The review and PR-closure triggers have the connected repository's ID. Maintainers know to review and update fork PRs outside Factory, rather than ask Factory for changes in their comments.
  • GitHub requires a human approval before merge, including after changes to the PR.
  • The disabled automatic paths and duplicate Loops don't start work.

Assign someone to watch usage and credits and stop unexpected work. Agree who will pause Loops and cancel runs. A simultaneous-run limit isn't a total-spend cap. The controls above reduce risk, but public content can still try to redirect an agent after a trusted maintainer starts work.

Stop work

Use these steps for any Factory repository when you need to stop new work and cancel existing work. Loops are called schedules in the CLI.

Stop new starts

  • Settings created in Tessl: pause each affected Loop and verify its status:

    tessl schedule pause <loop-id>
    tessl schedule view <loop-id> --json
  • To disable all repository-defined Loops in this project: open the project's Settings page and turn off Manifest automations. Confirm the change, then check that the switch is off. This doesn't stop Loops created directly in Tessl or cancel active runs. Pause those Loops separately and cancel existing runs as described below.

  • To remove selected work from tessl.json instead: remove every affected starting entry from both triggers and schedules, including CI events and cron schedules. Match its action string or action.use value, and check inline actions and copies. Remove the affected named actions entries too, so direct action-trigger calls can't start them. See action references and inline actions.

Merge the repository change, then verify it has been applied as in step 5. Until synchronization succeeds, the old settings can still start work. Confirm the affected entries in all 3 blocks are gone. Removing a PR's Factory label alone doesn't stop CI-triggered work.

Cancel existing runs and confirm they stopped

Pausing a Loop doesn't cancel existing runs. List runs in the Tessl workspace, then cancel each one belonging to this repository:

tessl launch list --workspace <workspace> --status pending --limit 100 --json
tessl launch cancel <launch-run-id> --json
tessl launch view <launch-run-id> --json

Match data[].attributes.config.repoUrl. Follow nextCursor with --after <cursor> for more results. Use launch view until each run is cancelled, completed, or failed. Re-list after disabling the Loops.

"Cancellation requested" doesn't mean execution has stopped. If a run remains active, contact Tessl support. Cancellation doesn't undo changes. Inspect suspicious code/CI changes and rotate exposed credentials.