LocalCI

Run your project's existing checks on your computer, keep the output in your terminal, and report the verdict against the Git commit the checks ran on. LocalCI can show a passing or failing status on GitHub pull requests. Reporting is optional and off by default: with no reporter, a run is a local check that keeps everything on your machine.

LocalCI works with any language and any check command. Your code can stay on GitHub or another Git host. You do not need a GitTrove-hosted repository, a GitTrove framework, a hosted runner, or a workflow file.

Before you start

You need:

LocalCI runs commands from the repository root, even when you invoke it from a subdirectory. It uses your current environment and PATH. It does not sandbox commands, so run it only on checkouts you trust.

Install

  1. Visit the LocalCI listing for the current price, subscribe, and create a key in your GitTrove dashboard. The key is shown once.

  2. Install uv if you do not already use it.

  3. Store the key in your secret manager. For example, with gopass, run gopass insert gittrove/localci and paste the key at its prompt. Then install:

    UV_DEFAULT_INDEX="https://__token__:$(gopass show -o gittrove/localci)@pypi.gittrove.com/simple/" \
      uv tool install --python 3.13 LocalCI
    localci --version

The examples use a POSIX shell, such as Bash or Zsh. With a different secret manager or shell, substitute your own retrieval command for $(gopass show -o gittrove/localci). Keep keys out of repository files and command history. You do not need to use gopass to run LocalCI.

uv tool install gives the command its own environment. If localci is not found afterward, run uv tool update-shell, reopen your terminal, and try again. If you already manage a Python 3.13 or 3.14 virtual environment, you can instead run this in that environment:

PIP_INDEX_URL="https://__token__:$(gopass show -o gittrove/localci)@pypi.gittrove.com/simple/" \
  python -m pip install LocalCI

Installer credentials are supplied through environment variables rather than command-line arguments. LocalCI has no runtime package dependencies.

Your first run

Choose the command you already use to check your project. In the repository root, add a [tool.localci] table to pyproject.toml. If your project does not have this file, create it with just this table. This works for non-Python projects too.

For a Python project managed by uv:

[tool.localci]
command = ["uv", "run", "--locked", "python", "-m", "pytest"]

Other examples:

Your existing check command Configuration
npm test command = ["npm", "test"]
make check command = ["make", "check"]
cargo test --locked command = ["cargo", "test", "--locked"]
just check command = ["just", "check"] (the default)

Each element is one argument. Commands are not interpreted by a shell, so &&, pipes, wildcards, and $VARIABLE are not expanded. For multiple checks, configure one script or task that runs all of them and exits nonzero if any fails.

Run the checks:

localci
LocalCI: acme/widget at 0632064b651f
LocalCI: running npm test
...
LocalCI: npm test passed

LocalCI gets its owner/name from origin, or from the sole remote if there is only one. For a local repository without a remote, or a host with nested groups, set repository = "acme/widget" explicitly.

Nothing is sent anywhere by that run. A working-tree run checks the files as they stand, and the closing line says so when there are uncommitted changes. To execute the checks without acting on the result, and without any credential:

localci --dry-run

With GitHub reporting enabled, the dry run prints the status payload instead of posting it.

Committed profiles and retained evidence

Use localci validate for ordered checks from a committed snapshot, with optional pinned Docker execution and retained logs/artifacts. Use localci report to publish verified execution evidence later. The execution guide shows configuration, overrides, failure behavior, and the versioned record. These commands do not manage application previews or deployment policy.

Adopt LocalCI in an existing project

An existing project needs no new checks, no workflow file, and no change to how it installs its dependencies. Point LocalCI at the command the project already runs, then verify it:

Follow LocalCI for coding agents for the complete installation, configuration, and verification contract. By default, install in isolation with an authorized existing credential, preserve configuration, run full existing checks once with localci --no-github, verify failure propagation, and leave a project instruction. GitHub reporting and committed execution profiles are opt-in. Report failing checks separately from setup failures and follow the project's policy for commits and pushes.

Show results on GitHub pull requests

Follow the GitHub setup guide. In brief:

  1. Sign in with gh auth login, or set GITHUB_TOKEN to a token permitted to write commit statuses on the repository.
  2. Add github = true to your existing [tool.localci] table.
  3. Commit all changes, push the commit, and run localci.
LocalCI: acme/widget at 0632064b651f
LocalCI: running npm test
...
LocalCI: reported success to GitHub as 'LocalCI'

GitHub reporting requires a clean working tree, including no untracked files. Checks that change tracked files or create unignored files cannot publish a status. Ignore disposable test artifacts, and use check modes rather than formatters that rewrite source. If HEAD changes while checks run, LocalCI publishes nothing. --sha can assert the expected HEAD; it cannot relabel a run as a different commit.

To check work in progress, use localci --no-github, or localci --no-github --dry-run to keep it entirely local.

A pending status while the checks run

GitHub shows one status per context, so a verdict only appears when a run finishes. To make a run visible while it is happening, and to replace an earlier green status as soon as a new run starts, add github_pending = true:

[tool.localci]
command = ["npm", "test"]
github = true
github_pending = true

LocalCI then posts pending before the checks start and the verdict when they finish. A run that cannot start, or times out, closes the pending status with an error instead, so nothing is left waiting. Interrupting with Ctrl-C publishes nothing, so a pending status can stay until another run replaces it.

This is off by default because pending is a real gate: a required status that is published and never replaced blocks the pull request. It is also the one status that needs reporting, so github_pending with github = false is a configuration error rather than a silent no-op.

Stopping a check that hangs

A check that waits on a prompt or a network call has no deadline unless you set one:

[tool.localci]
command = ["npm", "test"]
timeout_seconds = 900

LocalCI stops the command and everything it started after that many seconds, gives it a few seconds to exit, then forces whatever is left. The run reports timed out after 900s, exits 1, and never posts success. --timeout SECONDS overrides the setting for one run, and --timeout 0 removes the deadline.

Termination reaches the command and the processes it started on macOS and Linux, where each run gets its own process group. On Windows the command gets its own group as well, but a signal reaches only the command itself, so a process it started can survive a timeout or an interrupt. LocalCI cannot prove that a container or a service the command launched somewhere else has stopped either; a project-owned Docker command still owns its own cleanup. Because the command runs in its own session it does not receive the terminal's own Ctrl-C; LocalCI stops it and reports the interruption itself.

A GitHub status is a report from a developer's machine. It does not independently verify the developer's environment or prevent someone with status-write access from posting another result. Keep hosted CI for any independent validation your team requires. Avoid editing files concurrently with a run; LocalCI checks the checkout before and after execution, not every filesystem change in between.

Configuration and options

All configuration lives in the root pyproject.toml. Unknown option names are errors, so a typo cannot silently select the defaults.

Setting Default Purpose
command ["just", "check"] Executable and arguments for your checks.
repository inferred from remote Repository identifier as owner/name.
github false Post a GitHub commit status for the verdict.
github_context "LocalCI" GitHub status name, including for branch rules.
github_pending false Post a pending status before the checks run. Needs github.
timeout_seconds unset (no deadline) Stop the checks after this many seconds.
github_url "https://api.github.com" Advanced: GitHub Enterprise API base URL.

Credentials come from the environment, never the configuration table. Only use API URLs you trust; LocalCI sends the corresponding token to the configured URL. Use HTTPS for remote services.

CLI option Purpose
--repository OWNER/NAME Override the configured or inferred identifier.
--sha SHA Require that HEAD equals this full 40-character commit SHA.
--command ARGUMENT Replace the command, repeating the flag for each argument. Use --command=-q for an argument starting with -.
--timeout SECONDS Stop the checks after this many seconds. 0 means no deadline, which overrides a configured timeout_seconds.
--github / --no-github Enable or disable GitHub reporting for this run.
--github-pending / --no-github-pending Enable or disable the pending status for this run.
--dry-run Execute checks and print the payload without API requests or credentials.
--help / --version Show help or the installed version.

Failures and exit codes

Code Meaning and next step
0 Checks passed, and GitHub accepted the status when reporting was enabled.
1 Checks failed or timed out. LocalCI still attempts to report the failure unless this is a dry run. Read any reporting errors too.
2 Configuration or startup failed. Fix the displayed problem and rerun.
3 No trustworthy result could be published, or the GitHub status failed after the checks passed. Read the error to see what is missing.
130 Checks were interrupted with Ctrl-C. No result was published.

A check outcome always wins over a reporting problem: a run whose checks failed or timed out exits 1 even when its status could not be posted.

Common fixes:

For help, email audrey@feldroy.com. Include localci --version, your OS, your check command, and the error with credentials removed. Never include keys or private check output.

What leaves your machine

Without GitHub reporting, LocalCI sends nothing: the checks run on your machine and the verdict is printed to your terminal. With GitHub reporting enabled, LocalCI sends the repository identifier, full commit SHA, state, status name, and a description containing the command including its arguments to GitHub's commit-status API. With github_pending enabled it sends the same fields once at the start of the run, with the state pending.

Check output and source files are not uploaded by LocalCI. Do not put secrets in command arguments because those arguments appear in the posted description. Pass secrets through the environment or your secret manager instead. The checks LocalCI invokes may make their own network requests.

License

LocalCI is proprietary software owned by Feldroy, LLC. A paid subscription grants the right to use it under the GitTrove terms of service, which cover your account, the private index, and every package sold through it. Those terms ship with LocalCI as the bundled LICENSE notice. The price is 29 USDC every 30 days, collected from your wallet until you cancel.