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

# Projects

> Give an agent a codebase or a file tree to work in, from an upload or a Git repo, with immutable revisions and a disposable sandbox overlay.

A **Project** is a reusable, Workspace-scoped filesystem tree an agent works in. Mount it into a [Session](/concepts/sessions-and-runs) and its files appear at a path in the sandbox. A single uploaded file and a whole repository share one shape: a file is just a one-entry Project.

## Two sources

| `source.kind` | What it is                    | Carries                                                                                      |
| ------------- | ----------------------------- | -------------------------------------------------------------------------------------------- |
| `upload`      | A file tree you push directly | `files` (1–1,024 entries of normalized relative paths per uploaded revision)                 |
| `git`         | A GitHub repository           | `provider`, `repository` (`owner/repo`), `connection_id`, `ref`, and an `acted_as` Principal |

A Git Project resolves through a [Connection](/concepts/capabilities): Checkfu reads the repository using the acting Principal's authorized provider identity and never handles a raw provider token. `acted_as` must own and hold `use_connection` on that Connection.

## Immutable revisions

You never edit a Project in place. Every change produces a new immutable **revision**:

* An uploaded Project gets a new revision when you push new `files` (with `expected_version` for optimistic concurrency).
* A Git Project gets a new revision when you `POST /v1/projects/{id}/refresh`, which re-resolves the `ref` to an exact commit and records the `commit_sha` (and tree).

A revision records its content hash, file count, and byte size, so what an agent saw is inspectable long after the fact.

## Create and refresh a Git Project

Create the Project with the Principal that owns the GitHub Connection:

<CodeGroup>
  ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
  curl "$CHECKFU_API_URL/v1/projects" \
    --request POST \
    --header "Authorization: Bearer $CHECKFU_API_KEY" \
    --header "Checkfu-Version: $CHECKFU_VERSION" \
    --header "Idempotency-Key: $(uuidgen)" \
    --header "Content-Type: application/json" \
    --data '{
      "name": "support-site",
      "acted_as": "'"$PRINCIPAL_ID"'",
      "source": {
        "kind": "git",
        "provider": "github",
        "repository": "acme/support-site",
        "connection_id": "'"$CONNECTION_ID"'",
        "ref": "main"
      }
    }'
  ```

  ```ts TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
  const project = await checkfu.projects.create(
    {
      name: "support-site",
      acted_as: principalId,
      source: {
        kind: "git",
        provider: "github",
        repository: "acme/support-site",
        connection_id: connectionId,
        ref: "main",
      },
    },
    { idempotencyKey: crypto.randomUUID() },
  )
  ```
</CodeGroup>

Creation resolves and publishes revision 1, so the create response reports `latest_revision: 1`
and `version: 2`: the revision counter and the resource version are separate, and publishing that
first revision already moved the version. Read `GET /v1/projects/{id}/revisions/1` for its exact
commit and tree. When the ref advances, refresh with the Project's current `version`:

<CodeGroup>
  ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
  curl "$CHECKFU_API_URL/v1/projects/$PROJECT_ID/refresh" \
    --request POST \
    --header "Authorization: Bearer $CHECKFU_API_KEY" \
    --header "Checkfu-Version: $CHECKFU_VERSION" \
    --header "Idempotency-Key: $(uuidgen)" \
    --header "Content-Type: application/json" \
    --data '{
      "expected_version": 2,
      "acted_as": "'"$PRINCIPAL_ID"'"
    }'
  ```

  ```ts TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
  const refreshed = await checkfu.projects.refresh(
    projectId,
    { expected_version: 2, acted_as: principalId },
    { idempotencyKey: crypto.randomUUID() },
  )
  ```
</CodeGroup>

If the Project changed concurrently, `expected_version` fails with `409`; read the Project again
before retrying. If the ref still identifies the recorded commit and tree, refresh also returns a
conflict rather than creating a duplicate revision.

Use `GET /v1/projects/{id}/revisions/{revision}` for the exact revision and
`GET /v1/projects/{id}/revisions/{revision}/tree` for its ordered file metadata. The public
`/file` body endpoint serves uploaded revisions only. Git bodies stay behind the live Run lease so
each read can re-check Connection authority and integrity immediately before staging.

## `latest`, a pinned revision, or a published Git ref

A Session mounts a Project with `latest`, a `pinned` revision, or a Git `ref` policy. This is one half of the platform's [two freeze horizons](/concepts/sessions-and-runs#two-freeze-horizons):

* **`latest`** re-resolves to whatever the newest revision is **at the start of each Run**, and is fixed for that Run. Refresh the Git Project between Runs and the next Run picks up the new commit.
* **`pinned`** locks one revision for the Session's life, regardless of later refreshes.
* **`ref`** selects the newest immutable revision already published for that exact Git ref. It
  performs no provider I/O during Run preflight and fails closed when no such revision exists.

Either way, the exact revision a Run received is recorded in that Run's `session.mounts_resolved`.

### Repository Skills

At the same Run boundary, an eligible Git mount inspects exactly
`.claude/skills/<name>/SKILL.md` in that frozen revision. Valid manifests use
the same Agent Skills parser as catalog Skills, but they remain repository
content at their mounted paths: Checkfu creates no Skill aggregate or release
pin. Duplicate names remain available by path. Nested, rootless, package-local,
malformed, and name-mismatched entries are not announced.

When the Agent has the built-in `read` tool, Checkfu stages a read-only index at
`/environment/.checkfu/repository-skills.md` and adds a bounded pointer to it.
Without `read`, the pointer is omitted. Treat mounting as a trust decision:
repository Skills are instruction input and may include contributions from
people outside your organization.

## Writable overlays and opt-in pull-request writeback

A mount is `read_only` or `read_write`, but even `read_write` does not mutate the Project. Runtime writes land in a **disposable sandbox overlay**: they exist for that Run and vanish with the sandbox. Publishing a new uploaded revision or pushing a Git tree is a separate, explicit, governed action.

For a Git-backed Project under standard retention, a `read_write` mount can opt into Session-owned
publication with `writeback: { "kind": "pull_request" }`. Checkfu captures the complete delta at
Run settlement, closes the sandbox, and reconciles it without holding compute: one
platform-derived branch, non-force exact-head updates, one pull request, and one immutable branch
revision selected only by that Session mount. A missing policy remains byte-for-byte the
disposable-overlay behavior; uploaded Projects and ZDR reject writeback.

The policy accepts bounded static `commit_message`, `pull_request_title`, and
`pull_request_body` overrides. Omitted values use deterministic platform defaults; an omitted body
uses a bounded default PR template from `.github/`, the repository root, or `docs/` at the frozen
target ref when one is present. Provider credentials, hidden reasoning, and event history never
enter the commit or PR body.

Standard interactive Sessions attach one reusable Watch after the PR's fresh baseline is known,
so external comments, reviews, and checks can wake the same Session and the next writeback can
advance the same branch and PR. D127 still excludes Automation-owned Sessions
from Watch-driven runs; their writeback produces the PR receipt without claiming an unsupported
feedback wake.

## Write back to Git

A Git Project is not read-only. Two governed endpoints turn a tree you have staged into a branch and then a pull request, through the same Connection that `refresh` reads with. Checkfu still never handles a raw provider token, and `acted_as` must own the Connection and hold `use_connection`.

<CodeGroup>
  ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
  curl "$CHECKFU_API_URL/v1/projects/$PROJECT_ID/push" \
    --request POST \
    --header "Authorization: Bearer $CHECKFU_API_KEY" \
    --header "Checkfu-Version: $CHECKFU_VERSION" \
    --header "Idempotency-Key: $(uuidgen)" \
    --header "Content-Type: application/json" \
    --data '{
      "acted_as": "'"$PRINCIPAL_ID"'",
      "base_revision": 2,
      "branch": "agent/fix-signup-copy",
      "message": "Fix signup copy",
      "files": [
        { "path": "src/copy.ts", "content_base64": "'"$CONTENT_BASE64"'" }
      ]
    }'
  ```

  ```ts TypeScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
  const pushed = await checkfu.projects.push(
    projectId,
    {
      acted_as: principalId,
      base_revision: 2,
      branch: "agent/fix-signup-copy",
      message: "Fix signup copy",
      files: [{ path: "src/copy.ts", content_base64: contentBase64 }],
    },
    { idempotencyKey: crypto.randomUUID() },
  )
  ```
</CodeGroup>

`POST /v1/projects/{id}/push` writes one commit and returns `201` with the new `branch`, its `commit_sha`, the `base_revision` and `base_commit_sha` it was built on, plus `file_count` and `total_bytes`. `POST /v1/projects/{id}/pull-requests` then takes `head`, `base`, `title`, and an optional `body`, and returns the pull request `number` and `url`. Provider validation refusals (no commits between the two branches, or a pull request already open for them) come back as a reasoned `409 validation.conflict`. The merge itself stays human.

Both calls require `Idempotency-Key`. Retry an interrupted response with the same key and the identical request: after the live lease expires, Checkfu returns the retained provider result without repeating the push or opening a second pull request.

Three rules make the write safe to automate:

* **`base_revision` is a compare-and-swap.** It names an immutable revision of this Project, and that revision's exact commit must still head the Project's source `ref`. If the ref moved, the push refuses with `409 validation.conflict` rather than silently dropping the newer work. Refresh, rebuild the tree on the new revision, and push again.
* **A push only ever creates a branch.** It never force-pushes and never updates an existing one, so a replayed push cannot overwrite a colleague's branch. The destination ref is checked before any blob is uploaded, so a name collision fails early with `409 validation.conflict` instead of after the whole tree is written.
* **Neither call touches the Project.** The source ref is never advanced, no revision is published, and the Project's `version` does not move. Adoption stays a human merge plus an explicit `refresh`. Each success appends one audit fact (`project.pushed` or `project.pull_request_opened`) that records coordinates, never file bodies.

A push manifest is capped at **64 files**, lower than the 1,024 entries an uploaded revision accepts, because each file becomes its own content mutation against GitHub's per-minute budget. The per-file and per-revision byte limits are the same as an uploaded revision's; the [limits table](/reference/lookup#request-and-payload-limits) has both. Split larger work across pushes to separate branches.

<Note>
  Because a push stores no file bodies (the tree streams to the provider and Checkfu retains only
  coordinates and hashes), it works in a zero-data-retention Workspace, where publishing an uploaded
  revision does not.
</Note>

<Note>
  Managed runs materialize both uploaded and Git Project revisions through a credential-free,
  integrity-checked handoff. A Git plan contains the pinned commit, tree, and ordered blob
  locators, not a provider credential. The API re-checks the live Connection PermissionAssignment for each
  lease-fenced blob read, and both the API and Runner verify its Git digest and byte count before
  staging it.
</Note>

## Next steps

<CardGroup cols={2}>
  <Card title="Sessions and runs" icon="timeline" href="/concepts/sessions-and-runs">
    How a mount resolves per Run, and the two freeze horizons.
  </Card>

  <Card title="Capabilities" icon="plug" href="/concepts/capabilities">
    The Connection a Git Project reads through.
  </Card>
</CardGroup>
