# Add JWT Auth to a Hono API from a Linear Issue

Yes. An agent picks up the Linear issue, protects your Hono API's write routes with Hono's jwt middleware, adds tests and opens a pull request.

[Run this on Rush](https://cloud.getrush.ai/run/add-jwt-auth/hono/), in a Project that has your repository.

## What the agent does

The agent starts from the issue and the repository, cloned into its own sandbox, and reads how requests reach the routes before it adds middleware, because in Hono the order of `app.route` and `use` calls decides what a middleware covers. In the `blog` example of honojs/examples (a Cloudflare Worker on `hono` `^4.2.4` with a KV namespace bound as `BLOG_EXAMPLE`), `src/api.ts` defines `GET /posts`, `POST /posts`, `GET`, `PUT` and `DELETE /posts/:id` on its own `Hono` instance, and `src/index.ts` mounts it with `app.route('/api', api)`. The same file already guards those writes: a `middleware` app runs `basicAuth({ username: c.env.USERNAME, password: c.env.PASSWORD })` on `/posts/*` for any method other than `GET`, and is mounted on `/api` before `api`. So the issue is really "replace basic auth with JWT", and the agent says that in its plan.

It also finds why nobody noticed the basic auth: the tests never see it. `src/api.test.ts` imports `worker from '../src/api'`, the inner app, not `src/index.ts`, and calls `worker.request('/posts', ...)` with `env` from `cloudflare:test`, so all ten tests run with no auth in front of them and `POST /posts` returns 201 without credentials. A JWT check added in `index.ts` the same way would be just as untested. The agent therefore writes the new tests against the app as deployed, `src/index.ts` with its `/api` prefix, or moves the guard into `api.ts` beside the routes it protects, and states which in the pull request.

The change uses Hono's built-in `jwt` middleware from `hono/jwt`. Because the secret is a Worker binding and only exists per request, the middleware is created inside a handler, `(c, next) => jwt({ secret: c.env.JWT_SECRET, alg: 'HS256' })(c, next)`, on `POST /posts` and on `PUT` and `DELETE /posts/:id`, leaving the two reads public as the issue asks. `JWT_SECRET` replaces `USERNAME` and `PASSWORD` in the `Bindings` type in `src/bindings.ts`, a `.dev.vars` entry covers `wrangler dev`, and the README says to set it with `wrangler secret put JWT_SECRET`. The agent checks the body of the 401 the middleware returns, and if it is not the `{ error, ok: false }` JSON every other error in this API returns, adds an `onError` handler so clients get one shape.

Then it runs the tests the way the example is set up. `vitest.config.ts` uses `@cloudflare/vitest-pool-workers` with `wrangler: { configPath: './wrangler.toml' }`, and only `wrangler.example.toml` is committed, so the agent copies it as the README says and gives the test pool a `JWT_SECRET` binding. Tests sign tokens with `sign` from `hono/jwt` and check: a write with no token answers 401, a write with a token signed by another secret answers 401, an expired token answers 401, a valid token creates, updates and deletes a post, and `GET /posts` still needs nothing. It runs `npm test`, which is `vitest --run`, against the Workers runtime locally, with no Cloudflare account. The `blog` package has no build script, so it runs `tsc --noEmit` and `wrangler deploy --dry-run` to show the Worker compiles and bundles, and says so instead of claiming a build ran.

## Start it with one API call

`POST https://api.prix.dev/o/{org}/sessions`

```json
{
  "agent": "claude",
  "input": "A Linear issue labeled agent says: \"Add JWT Auth to a Hono API. In the blog example, protect the create, update and delete routes with Hono's jwt middleware and return 401 without a valid token.\" Before changing anything, trace how requests reach those routes: every app.route and use call, any auth already in front of them, and whether the existing tests exercise the app as deployed or an inner router. If another auth already guards the routes, replace it and say so. Use the jwt middleware from hono/jwt with the secret read from a Worker binding at request time and an explicit alg, keep the read routes public, update the Bindings type, .dev.vars and README, and make the 401 body match the API's other errors. Write tests through the deployed entry point that sign tokens with hono/jwt: no token, wrong secret, expired token and valid token for each protected route, and a public read. Run the example's test script and, since it may have no build script, tsc --noEmit and wrangler deploy --dry-run, until they pass. Open a pull request that links back to the issue and lists what was already there, what changed and why.",
  "resources": [
    {
      "url": "https://github.com/your-org/your-app"
    }
  ]
}
```

1. **Get an API key.** In the console, open Settings, then API keys, and create a key for your org. Send it in the Authorization header as a Bearer token.
2. **Send the request.** POST the body in request.json to https://api.prix.dev/o/{org}/sessions, with your repository as the resources URL. The run starts in its own cloud sandbox.
3. **Read what came back.** Follow the run on its live stream or wait for the run.completed webhook, then read the result and its trajectory.

## What comes back

What comes back is a pull request on its own branch that links the Linear issue. For the `blog` example it changes `src/index.ts` (the `basicAuth` guard replaced by `jwt`, or moved into `src/api.ts`), `src/bindings.ts` (`JWT_SECRET` in place of `USERNAME` and `PASSWORD`), `vitest.config.ts` for the test binding, the README's setup steps, and the tests. The description says plainly that the write routes were already protected by basic auth, that the existing tests never went through it, and how the new ones do.

The proof is the test output in the session's transcript, which keeps every command and its result: `npm install`, each `npm test` run against the Workers runtime, `tsc --noEmit` and `wrangler deploy --dry-run`, including any that failed before a fix. If your repository runs checks on pull requests, they run on this one too.

Before merging, a reviewer checks that each protected route answers 401 without a token in the tests, not only in the description; that the secret is read from a binding and never committed (`.dev.vars` belongs in `.gitignore`); that `alg` is pinned so a token signed with another algorithm is refused; and that whatever issues tokens to clients today is in place before basic auth goes away.

## Run it on every Linear issue labelled agent

In the console, an automation on the Project with a Linear trigger on `linear.issue.labeled` for the label `agent` starts a run for each issue that gets the label, in the Project's repository, with the issue as context. Auth changes like this one are worth a label because the issue says exactly which routes, and the pull request shows exactly which tests prove it.

Outside the console, your server sends one `POST /o/{org}/sessions` per issue, with the repository and the `https://linear.app/<workspace>/issue/<ID>` URL both in `resources`; Rush reads the issue through your Linear connection. When the turn finishes, a webhook on the API key posts `run.completed` or `run.failed` with the `session_url`.

## Why run it on Rush?

Testing auth on a Worker means running it in the Workers runtime with its bindings, a KV namespace and a secret, and signing real tokens against it. `@cloudflare/vitest-pool-workers` does that locally, and in a Rush sandbox the agent installs it, copies the wrangler config, sets a test secret and runs the suite on its own machine, with no Cloudflare account and nothing deployed.

The value of the result is what it shows a reviewer: not only that JWT checks were added, but that the old guard was there, that the old tests skipped it, and that the new tests go through the same entry point production does. All of that is in the transcript with the commands and their output, next to a pull request linked from the issue.

## Frequently asked questions

### Where does the JWT secret come from?

From a Worker binding, `JWT_SECRET`, read from `c.env` on each request. Locally it is set in `.dev.vars` for `wrangler dev` and as a test binding in the Vitest pool; in production you set it with `wrangler secret put JWT_SECRET`. The secret is never written into the code or `wrangler.toml`.

### Will it also issue tokens?

Not unless the issue asks. This issue is about checking tokens, so the pull request protects the routes and the tests sign their own tokens with `hono/jwt`. If your clients need a login route that returns a token, that is a separate issue with its own design choices.

### Why did the existing tests pass without credentials?

In the `blog` example they import `src/api.ts`, the inner router, while the auth middleware is mounted in `src/index.ts`. Requests in those tests never pass through it. The agent's tests go through the deployed entry point so a missing guard fails a test.

### Does it work for Hono on Bun or Node, not only Workers?

Yes. The `jwt` middleware is the same; what changes is where the secret comes from (an environment variable instead of a binding) and how the tests run (`bun test` or Vitest without the Workers pool). The agent reads the runtime from the repository and follows it.
