hammerkit

Remote

Register the servers hammerkit can send runs to and choose the default location.

Experimental. This page covers the registry and signing in. Sending a run to a server (--on, running a task with a runner:) arrives in follow-up releases, see the remote compute spec.

remote manages the list of hammerkit servers your machine knows about. Each one is registered under a short name, which later features (--on <name>, a task's runner:) refer to instead of a URL.

hammerkit remote add build https://build.example.com --issuer https://login.example.com
hammerkit remote login build
hammerkit remote accounts build
hammerkit remote publish payments build
hammerkit remote approvals build
hammerkit remote approve run_Qx3k9ZpL2mW7vN4a build
hammerkit remote list
hammerkit remote use build
hammerkit remote remove build

Except for publish, the commands only touch your host config and talk to the server, so they work with or without a build file in the working directory.

Commands

CommandWhat it does
remote add <name> <url> --issuer <url>Register a server. --issuer is the OAuth issuer the server accepts tokens from, written exactly as the iss of its tokens (some providers end it with a slash). --client-id, --audience and --scope pin what a login asks for, see Signing in.
remote listShow the registered servers and which one is the default.
remote use <location>Set the default location: local, auto or a registered name.
remote remove <name>Forget a server. If it was the default, runs are local again.
remote login <name>Sign in to the server with your identity provider.
remote logout <name>Forget the sign-in.
remote accounts [name]Show who you are on the server and which service accounts you may use. Without a name, the default server.
remote publish <definition> [server]Publish the build file of the working directory (or the one --file names) and the files it includes under a name, see Publishing.
remote approvals [server]List the runs that wait for your approval.
remote proposals [server]List the proposed versions of build files you made or may approve, see Proposing.
remote diff <name@hash> [server]Show what a proposed version changes.
remote approve <run or name@hash> [server]Let a run go ahead, or publish a proposed version. --reason <text> is optional.
remote reject <run or name@hash> [server]End a run, or drop a proposed version. --reason <text> is shown to who asked for it.

Re-running remote add with the same values changes nothing. To point an existing name at a different server pass --force, so a typo can't silently redirect your runs.

local and auto are reserved and can't be used as server names.

Host config

Registrations live in ~/.hammerkit/config.yaml. Set HAMMERKIT_CONFIG to use a different file.

servers:
  build:
    url: https://build.example.com
    issuer: https://login.example.com
run:
  on: build

The config is deliberately not part of the project's build file. A checked-in file naming a server URL could redirect your source code and OAuth tokens, so the decision of where to send work stays on the machine.

URLs

Server URLs must use https. Plain http is accepted only for localhost, 127.0.0.1 and [::1], for local testing. URLs with credentials, a query string or a fragment are rejected, and a trailing slash is dropped.

Publishing a build file

A server runs published build files, it does not take task definitions from the one who runs them. remote publish sends yours:

$ hammerkit remote publish payments build
published payments to build: sha256:9f2c61d0... (3 files)
tasks and services: build, deploy, more:lint

It reads the build file with the parser of a run and sends it together with every local file it includes or references, as text. That is the bundle, up to 200 files and 1 MiB. Includes have to be below the directory of the build file, because a server could not reproduce anything else; git includes are not sent, the server fetches them and wants them pinned to a commit. The version printed is the hash of the bundle, computed here the same way the server computes it, so the same files always have the same version. Publishing what a server already has says already published and changes nothing.

You need the publish permission for that name on the server, see entitlements. Without a server name the default server is used, and --file <path> publishes another build file.

Proposing a version

With only the propose permission for the name, the same command proposes the version. It does not become a version and can not run until somebody who may publish that name approves it:

$ hammerkit remote publish payments build
proposed payments to build: sha256:4be1a07c... (3 files)
tasks and services: build, deploy, more:lint
it can not run before somebody who may publish payments approves it, until 2026-10-11T11:41:00.000Z

The one who may publish reads what changes before deciding:

$ hammerkit remote proposals build
build: 1 proposed
payments@sha256:4be1a07c...  proposed by coding-agent, waits for your approval
  tasks and services: build, deploy, more:lint
  decide before 2026-10-11T11:41:00.000Z
$ hammerkit remote diff payments@sha256:4be1a07c... build
payments@sha256:4be1a07c... on build, proposed by coding-agent (https://login.example.com)
compared with sha256:9f2c61d0...
--- .hammerkit.yaml (changed)
  tasks:
    build:
-     cmds: [npm run build]
+     cmds: [npm run build, ./upload.sh]
$ hammerkit remote approve payments@sha256:4be1a07c... build
approved payments@sha256:4be1a07c... on build, it is published

The diff shows the lines that changed with two lines around them, in full and with control characters replaced by ?, so nothing hides in it. It shows the files of the bundle, not what the commands in them do. remote reject drops the proposal, with a --reason the one who proposed it reads in remote proposals. Nobody approves their own proposal, and what you may not decide does not exist for you.

Approving runs

A run on a service account that its caller may only request waits for an admin of that account to approve it. If you are an admin of the account:

$ hammerkit remote approvals build
build: 1 waiting for your approval
run_Qx3k9ZpL2mW7vN4a  requested by 4f1c2b (https://login.example.com)
  payments sha256:9f2c61d0...
  tasks: deploy
  account op-payments-prod, source ref refs/heads/main, env=staging
  needs the approval of an admin of op-payments-prod
  decide before 2026-10-11T10:41:00.000Z
$ hammerkit remote approve run_Qx3k9ZpL2mW7vN4a build
approved run_Qx3k9ZpL2mW7vN4a on build, it is queued

What you see is what the decision rests on: who asked, which version of which build file, the tasks, the service account and where the code comes from. The version is the one the run is pinned to, so what you approve is what runs. You see the runs on the accounts you are admin of, and no others.

Signing in

remote login uses the OAuth device authorization grant, so it works on a machine without a browser:

$ hammerkit remote login build
signing in to https://build.example.com through https://login.example.com
  client hammerkit-cli, audience hammerkit, scope openid api://hammerkit/.default
To sign in, open https://login.example.com/activate and enter the code ABCD-EFGH
signed in to build as 4f1c2b
service accounts: op-payments-dev, op-web-dev

Open the address on any device, sign in as you normally do, and enter the code. The server tells hammerkit which identity provider client, audience and scope to use, so there is nothing to configure beyond remote add. The --issuer you registered has to be one the server accepts, otherwise the login stops before the browser step.

The server is only trusted with that once. The first successful login pins the client, audience and scope in the host config, and a later login refuses a server that asks for something else (register it again with remote add --force if the change is expected). Look at the line printed before the code: a client or audience you do not recognise, such as one of a cloud provider's own APIs, means stop. To not rely on the first use, give them to remote add (--client-id, --audience, --scope), the values come from whoever runs the server. Before a token is sent to the server, hammerkit checks that it was issued by the registered issuer for the pinned audience, so a token for another service never leaves your machine. offline_access is dropped from the scope: no refresh token is requested.

What is stored is one short-lived access token in ~/.hammerkit/credentials.json (HAMMERKIT_CREDENTIALS picks another file), readable by you only (a file others can read is not used; hammerkit says how to fix it). A refresh token is not kept: when the token expires, remote login again. The token is bound to the URL and issuer it was issued for, so registering the name again for another server (remote add --force) does not send the old token there.

Agents and CI do not use login. They bring their own token, an agent from its own client credentials and a CI job from the OIDC token of the job, and the server maps it to service accounts through its entitlements.

On this page