AI code review for Appwrite. Hansi reviews your GitHub pull requests, checks its own findings against the code before it posts them, and answers questions in the thread.
Hansi is the service behind Review in the Appwrite Console. Connect a GitHub repository to a project, and every pull request gets a review: inline comments on the lines that matter, a merge-confidence tier, and an approval or a request for changes. Mention Hansi on a pull request and it answers.
Hansi runs on Appwrite Cloud, and is an opt-in service for self-hosted Appwrite. Appwrite owns everything around a review: the GitHub App and its webhooks, which project reviews a repository, accounts, billing and the Console. Hansi owns everything between "review pull request #N at commit X" and the review on GitHub.
Table of Contents:
- Reviews that read the code - An agent checks out the pull request and reads the repository with
gitandripgrep, not only the diff, so a finding can point at the caller it breaks. - Verified findings - A second pass verifies every finding against the code before anything is posted. Findings are placed on lines the diff can actually comment on.
- Commands in a sandbox - With an open-runtimes orchestrator, the agents can run tests, type checkers and short scripts in an isolated sandbox of the pull request head. Nothing from a reviewed repository runs in the Hansi container.
- Decisions made by code - The model proposes a merge-confidence tier; open findings cap it, and whether to approve or request changes is decided by the code, never by the model.
- Chat on pull requests - Mention Hansi on a pull request or in a review thread, or reply to one of its findings, and it answers once, with the same read-only tools as a review.
- Configurable per repository - A
.hansi.jsonfile on the base branch sets the profile, paths, severity thresholds and extra instructions. A pull request cannot change how it is reviewed. - Built on Appwrite's queue - Jobs are read with utopia-php/queue, the same way Appwrite's own workers read a queue, so attempts, backoff and dead letters behave the same.
On Appwrite Cloud, open your project in the Console, go to Review, and connect a GitHub repository. New pull requests and pushes to them are reviewed from then on.
Every setting is optional. Add a .hansi.json to the base branch to change them, since it is read from there; a file that does not parse falls back to the defaults and the review says why. The file is the same one hansi.codes reads, and its JSON Schema gives editors completion.
{
"$schema": "https://hansi.codes/schema.json",
"reviews": {
"profile": "balanced",
"pathFilters": ["src/**", "!src/generated/**"],
"minSeverity": "minor",
"requestChanges": "major"
},
"instructions": "We target Node 22. Flag any use of synchronous filesystem APIs.",
"pathInstructions": [
{ "path": "migrations/**", "instructions": "Migrations must be reversible." }
],
"sandbox": { "setup": "npm ci" }
}| Setting | Default | Description |
|---|---|---|
reviews.enabled |
true |
Review pull requests in this repository. Chat follows this too. |
reviews.auto |
true |
Review when a pull request is opened or pushed to. Mentions work either way. |
reviews.drafts |
false |
Review draft pull requests too. |
reviews.baseBranches |
[] |
Only review pull requests into these branches. Empty means every branch. |
reviews.pathFilters |
[] |
Globs of files to review. A leading ! excludes, e.g. "!docs/**". |
reviews.profile |
balanced |
chill reports only bugs it can show; balanced adds risky patterns, weak tests and broken docs; strict adds naming and style. |
reviews.minSeverity |
minor |
Findings below this severity (info, minor, major, critical) are not posted. |
reviews.maxComments |
15 |
The most inline comments one review posts, up to 100. |
reviews.approve |
true |
Approve pull requests with nothing blocking and no open defect. |
reviews.requestChanges |
major |
Findings at or above this severity request changes. never only comments, and still never approves over a major or critical finding. |
reviews.approveOutsideContributors |
false |
Approve pull requests from authors without write access. Off by default: their code could try to talk the model into an approval. |
instructions |
"" |
Extra review instructions, in plain language. |
pathInstructions |
[] |
Instructions for files matching a glob. |
language |
en |
Language of review comments. |
sandbox.setup |
"" |
Shell command run in the sandbox before the review starts, e.g. npm ci. |
sandbox.network |
public |
public allows HTTP and HTTPS to public hosts; none removes the network. |
On a self-hosted Appwrite, Hansi is an optional container next to Appwrite's own. Appwrite publishes review jobs only when reviews are turned on; Hansi reads them from Appwrite's Redis and reports back over Appwrite's internal API, authenticated with the secret Appwrite sets as _APP_REVIEWS_SECRET.
Add the service to your Appwrite docker-compose.yml, on the same network as the appwrite and redis containers:
services:
hansi:
image: ghcr.io/appwrite/hansi:main
container_name: appwrite-hansi
restart: unless-stopped
stop_grace_period: 15m
networks:
- appwrite
depends_on:
- appwrite
- redis
environment:
- HANSI_APPWRITE_ENDPOINT=http://appwrite/v1
- HANSI_APPWRITE_SECRET=${_APP_REVIEWS_SECRET}
- HANSI_QUEUE_URL=redis://redis:6379
- HANSI_SHUTDOWN_TIMEOUT=840 # below stop_grace_period
- HANSI_REVIEW_MODEL=anthropic:claude-sonnet-4-5
- HANSI_AI_SECRETThen start it with docker compose up -d hansi. Without an orchestrator, as above, reviews read the code with the same tools but cannot run commands, and the agents are not offered run_command. To let them run commands, point HANSI_ORCHESTRATOR_ENDPOINT at an open-runtimes orchestrator and set HANSI_SANDBOX_IMAGE, e.g. to ghcr.io/appwrite/hansi-sandbox: Node 22, Python 3, PHP with Composer, Go and a C toolchain (sandbox/Dockerfile).
Images are published to ghcr.io/appwrite/hansi and ghcr.io/appwrite/hansi-sandbox for amd64 and arm64: main and sha-<commit> from every push to main, and a version tag for every release.
The service validates its environment on start (apps/service/src/env.ts) and exits with the reason when something is missing. The HANSI_QUEUE_* variables are read by the queue consumer (apps/consumer/consumer.php).
| Variable | Default | Description |
|---|---|---|
HANSI_APPWRITE_ENDPOINT |
required | Appwrite's internal API, e.g. http://appwrite/v1. Never the public endpoint. |
HANSI_APPWRITE_SECRET |
required | The _APP_REVIEWS_SECRET of that Appwrite. |
HANSI_QUEUE_URL |
required | Appwrite's queue: redis:// on self-hosted, nats:// on Cloud. |
HANSI_QUEUE_NAME |
v1-reviews |
The queue Appwrite publishes review jobs to. |
HANSI_QUEUE_USER |
Queue user, instead of in the URL. | |
HANSI_QUEUE_PASSWORD |
Queue password, instead of in the URL. | |
HANSI_REVIEW_MODEL |
required | The reviewer's model, as provider:model. See Models. |
HANSI_VERIFY_MODEL |
review model | The verifier's model, as provider:model with the review model's provider. |
HANSI_REVIEW_REASONING |
provider's | The reviewer's reasoning effort: none, minimal, low, medium, high, xhigh or max. |
HANSI_VERIFY_REASONING |
review effort | The verifier's reasoning effort. |
HANSI_AI_SECRET |
The provider's key. Required except for openai-compatible. See Models. |
|
HANSI_AI_REGION |
AWS region. Required for bedrock. |
|
HANSI_AI_BASE_URL |
provider's | The provider's API, instead of its default. Required for openai-compatible. |
HANSI_ORCHESTRATOR_ENDPOINT |
open-runtimes orchestrator API. Without it, reviews only read the code. | |
HANSI_ORCHESTRATOR_KEY |
Orchestrator API key, when it has one. | |
HANSI_SANDBOX_IMAGE |
Toolchain image for sandboxes. Required with an orchestrator. | |
HANSI_SANDBOX_RUNTIME_CLASS |
gvisor |
Sandbox isolation: gvisor, kata, or runc where the cluster has neither. |
HANSI_CONCURRENCY |
4 |
Jobs one container runs at once. Each holds a checkout, and a sandbox if it runs commands. |
HANSI_PORT |
8080 |
Port for /healthz and /readyz. |
HANSI_JOBS_PORT |
8081 |
Loopback port where the consumer hands over jobs. |
HANSI_SHUTDOWN_TIMEOUT |
3600 |
Seconds a stopping consumer waits for running jobs. |
HANSI_CONSUMER_COMMAND |
set by image | Starts the queue consumer. Unset, jobs only arrive from whoever calls the jobs port. |
HANSI_LOG_LEVEL |
info |
debug, info, warn or error. |
On Redis, which counts no deliveries, the consumer gives a job 3 attempts: the second 10 s after the first, the third 60 s after the second. On NATS the attempts and backoff are the ones Appwrite provisioned on the queue's work consumer.
A model id names its provider, then the provider's own model id: bedrock:global.anthropic.claude-sonnet-4-5-20250929-v1:0, openai:gpt-5. Both models use the same provider, with one credential in HANSI_AI_SECRET. The service checks on start that the provider has what it needs, and exits naming what is missing. Keys come from the environment until Appwrite serves model keys and models itself.
| Provider | HANSI_AI_SECRET |
Also needs |
|---|---|---|
bedrock |
A Bedrock API key, or an IAM key pair as ACCESS_KEY_ID:SECRET_ACCESS_KEY[:SESSION_TOKEN] |
HANSI_AI_REGION |
anthropic |
Anthropic API key | |
openai |
OpenAI API key | |
google |
Gemini API key | |
xai |
xAI API key | |
openrouter |
OpenRouter API key | |
openai-compatible |
Optional | HANSI_AI_BASE_URL |
Reasoning efforts go through the AI SDK's shared reasoning setting, which each provider maps to its own; OpenRouter gets its reasoning.effort option instead.
openai-compatible covers Ollama, vLLM, LiteLLM, Groq and other OpenAI-compatible APIs. On Bedrock, newer models are only served through an inference profile, so use the profile's id (global.…, us.…, eu.…) where one exists. A Bedrock API key is sent as a bearer token; an IAM key pair signs each request, and needs bedrock:InvokeModel and bedrock:InvokeModelWithResponseStream on the models and profiles used.
The image runs as the unprivileged hansi user with a read-only root filesystem in mind. In a cluster it needs:
- The environment above, with credentials from a Secret.
- A writable
/tmpfor checkouts. - Liveness on
/healthzand readiness on/readyz, port 8080. Readiness drops as soon as shutdown starts. - No Service: Hansi pulls jobs, nothing connects to it.
- A termination grace period long enough for running jobs to finish (a repository's setup command alone may take 15 minutes), with
HANSI_SHUTDOWN_TIMEOUTbelow it.
GitHub ─► Appwrite (webhooks, Review) ─► queue ─► Hansi
▲ │ installation token
└────── results, usage, trace ◄───────┤ review on GitHub
└─► orchestrator sandbox
- Appwrite receives a GitHub webhook for a repository with reviews turned on and publishes a job to its reviews queue: NATS JetStream on Cloud, Redis on self-hosted.
- The consumer, a PHP process on utopia-php/queue started by the service as its child, hands each delivery to the service over loopback and settles it by the outcome the service answers with.
- The service asks Appwrite for a repository-scoped installation token, checks out the pull request, and runs the review: an agent loop on the Vercel AI SDK, then a verify pass.
- The review is posted to GitHub with inline comments and a check run, and the results, model usage and trace go back to Appwrite. Calls that hit Appwrite's abuse limit are sent again once it resets.
The container runs and stops as one: Bun is PID 1, and on SIGTERM it stops the consumer, which stops receiving and settles what it holds once the jobs it handed over finish.
| Path | Description |
|---|---|
apps/service |
The process: environment, logger, health server, the jobs port, and the worker that runs jobs and decides retries. |
apps/consumer |
The queue consumer in PHP, on Broker\Nats (Cloud) or Broker\Redis (self-hosted). |
packages/engine |
The review and chat: the agent loop, its tools, the prompts, and placing, verifying and grading findings. |
packages/appwrite |
Appwrite adapters: the results sink, installation tokens and job payload parsing. |
packages/github |
GitHub client over REST and GraphQL, with installation tokens from Appwrite renewed before they expire. |
packages/sandbox |
Sandboxes on the open-runtimes orchestrator, for the agents' run_command. |
The engine depends only on its contracts (GitHubClient, SandboxClient, ResultsSink); adapters depend on the engine, never the other way round.
You need Bun 1.4, plus git and ripgrep on the path, which the review's tools run in tests too.
bun install
bun run test # all tests
bun run check # type-check every workspace
bun run lint # Prettier and ESLint
bun run format # apply Prettier
bun run dev # run the service with reloadTests use in-memory fakes and a scripted model from @hansi/engine/testing, and never call a real model, GitHub or Appwrite.
The consumer runs on PHP 8.5 with Swoole. Without a local PHP 8.5, run its tests in the image it ships on:
docker run --rm -v "$PWD/apps/consumer:/app" -w /app appwrite/base:2.2.7 \
sh -c 'composer install && vendor/bin/phpunit tests'To build and run the image locally:
docker build -t hansi .All code contributions, including those of people having commit access, must go through a pull request and be approved by a core developer before being merged. This is to ensure a proper review of all the code.
We truly ❤️ pull requests! If you wish to help, you can learn more about how you can contribute in the Appwrite contribution guide. Run bun run format, bun run lint, bun run check and bun run test before you push; CI runs the last three, the consumer's tests and an image smoke test.
Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests. Report them privately through GitHub private vulnerability reporting or by email to security@appwrite.io.
Whatever a review touches is treated as untrusted: file contents, pull request text, comments and command output. No credential reaches a sandbox's environment or the logs, and a sandbox's URL is a capability that is never logged.
This repository is available under the BSD 3-Clause License.