Try it on one laptop
Run Straza and Claude Code on your own computer. A starter rule denies a command, and you approve a held command in the browser.
- Who
- You, as the admin, and later as alice
- Where
- Four terminals and a browser
- Profile
- Standalone
In short
You start the server on your computer, enroll that computer and connect Claude Code. The starter rule denies rm -rf at once. A rule you add holds git push until you approve it yourself in this browser. The agent’s next try within 60 seconds then runs, and the approval covers only that try.
On this page
You run Straza on your own machine as a person named alice and govern her Claude Code. The standalone profile includes its own database, event broker and sign-in page, so nothing else has to run. A policy rule denies a command at once. Then a rule you write holds a command until you approve it.
Before you start
- A Linux or macOS machine with no Straza package and no managed install, because either one makes every
strazacommand use the system service. On Windows the service runs only as the Windows service, as Install shows. - The binaries
strazad,strazactlandstrazaon your PATH, as Put the binaries on your path shows. - Claude Code, a browser on this machine and an empty directory for the server.
Start the server
In the first terminal, in the empty directory, start the server on the standalone profile. On a shared network, such as an office or a café, put
STRAZA_APPROVER_TLS_AUTO_MINT=falsein front ofstrazad. That turns off the phone approver, which otherwise listens on port 8443 on every interface. This tutorial approves in the browser through port 8420 and does not need it.strazad serve --profile standaloneYou should see
The log says
strazad servingat127.0.0.1:8420. Above it are the passwords ofadminandbreak-glass, and a line that says the starter policy deniesrm -rf.Save both passwords now
The first start prints them once. A restart of the same data directory does not print them again.
Recorded output
It shows only the lines named above, with the passwords masked. The walk’s server listened on every address of its container and answered at
http://127.0.0.1:18420, as the final fold explains.{"level":"WARN","msg":"bootstrap admin created. Store this password now, it will not be shown again","username":"admin","password":"5861xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"} {"level":"INFO","msg":"starter policy seeded. Recursive force-delete (rm -rf) is denied with a reason; edit or delete PolicySet standalone-starter to change this"} {"level":"WARN","msg":"break-glass admin created. Store this password in your vault now, it will not be shown again; rotate on-box via `strazactl users set-password break-glass`","username":"break-glass","password":"95c4xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"} {"level":"INFO","msg":"strazad serving","addr":"[::]:8420","profile":"standalone","publicUrl":"http://127.0.0.1:18420","tls":false,"version":"v1.0.0-1700-g89d0cf1d"}The server keeps its state under
datain that directory. Its console and API listen on 127.0.0.1 only. Ports and network lists every port the server opens.Sign in to the console
Open
http://127.0.0.1:8420/console/in your browser and press Sign in. On the page titled Sign in to Straza, enteradminand the saved password.You should see
The console opens on Overview. The bottom of the menu says
standaloneand the server’s build.strazactlhas no sign-in of its own, so mint it an admin API token here.- Settings
- API tokens
- New API token
Name it
admin-terminal. Under Lifetime pickexpires in 30 days, under The job pick Full root, and press Mint token. Copy the token now, because the sheet shows it once and the server keeps only its hash.If this fails
Invalid username or password.- Use the
adminpassword from the first start of this data directory.
Add alice
In a second terminal, pass the token to
strazactl. At thereadline, paste the token and press Enter, which keeps it off the screen and out of the shell history. Then create alice, the person your agent works for. The rolestraza-enroll-browserlets her turn a browser into an approval device.export STRAZA_SERVER=http://127.0.0.1:8420 read -rs STRAZA_API_TOKEN && export STRAZA_API_TOKEN strazactl users create alice --password 'pick-a-passphrase' strazactl assign straza-enroll-browser --user aliceYou should see
created user alicewith an id, thenassigned straza-enroll-browser to alice.Recorded output
created user alice (01a19de0-43b0-7327-82a8-d66405f53f36) assigned straza-enroll-browser to aliceWhoever holds the token acts as an administrator until it expires or you revoke it, so keep it in this terminal only. Never put it in a shell profile or anywhere an agent runs.
If this fails
strazactl needs an admin API token in STRAZA_API_TOKEN.- Run the
readline again in this terminal and paste the token.
Start the background service
The background service,
straza service, keeps alice’s key and every Straza session that runs on this machine. WithSTRAZA_HOMEset, it runs as your own OS user and keeps its socket and its state in that directory instead of the system location.export STRAZA_HOME="$HOME/.straza" straza serviceYou should see
A log line that says
straza service listening, with the socket path anduser_mode=true.Enroll this machine as alice
Open a fourth terminal for alice. It shares
STRAZA_HOMEwith the service but has no token, so the Claude Code you start here cannot read the token.export STRAZA_HOME="$HOME/.straza" straza enroll --server http://127.0.0.1:8420Your browser opens on the sign-in page. Sign in as
alicewith her passphrase. The tab says that you are signed in to Straza on this computer.You should see
Enrolled as alice, the id of this device, and that the background service holds this machine’s key.The enrollment lasts 7 days from alice’s sign-in. After that the server refuses this machine’s check-ins until she runs
straza enrollagain.If this fails
Straza's background service for STRAZA_HOME … isn't running- Start
straza servicein Terminal 3, and export the sameSTRAZA_HOMEin both terminals. This machine has no display, so Straza cannot open a browser here.- Add
--use-device-code, open the link it prints, and sign in there as alice.
Connect Claude Code
In Terminal 4, install the hooks for your user, then start Claude Code in a throwaway directory.
straza install claude-code mkdir -p ~/straza-try && cd ~/straza-try claudeYou should see
installed Straza hooks for claude-codewith the settings path, then the bannerStraza governance active: alicewhen Claude Code starts.Ask Claude Code to run
rm -rf /tmp/x. The starter rule denies it, and Claude Code shows the reason,Straza starter policy: recursive force-delete is denied.A command that no rule names, such asgit status, runs.The same calls without Claude Code
Claude Code runs
straza hookbefore each tool call and reads its answer. To watch that without Claude Code, send the same events by hand in Terminal 4. The hook only returns a decision.printf '%s' '{"hook_event_name":"SessionStart","session_id":"laptop-1","cwd":"/tmp"}' | straza hook --harness claude-code printf '%s' '{"hook_event_name":"PreToolUse","session_id":"laptop-1","cwd":"/tmp","tool_name":"Bash","tool_input":{"command":"rm -rf /tmp/x"}}' | straza hook --harness claude-code; echo "exit $?"{"hookSpecificOutput":{"additionalContext":"Straza governance is active for this session. You are operating as \"alice\" (roles: straza-enroll-browser) against http://127.0.0.1:18420; policy snapshot c485b47b783a, attestation advisory. Tool use is checked locally against signed policy and every decision is audited. A denied tool call always carries its reason: relay it to the user and do not retry or work around the denial.","hookEventName":"SessionStart"},"systemMessage":"🛡 Straza governance active: alice (straza-enroll-browser) @ http://127.0.0.1:18420 · policy c485b47b783a · attestation advisory"} {"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Straza starter policy: recursive force-delete is denied. Delete files individually, or an admin can edit the standalone-starter PolicySet."}} Straza starter policy: recursive force-delete is denied. Delete files individually, or an admin can edit the standalone-starter PolicySet. exit 2Claude Code reads the JSON on standard output and shows you the reason line from standard error. Exit status 2 tells it not to run the tool.
Hold git push for yourself
A rule with the mode
confirmholds a call until the person the session acts for approves it. Nobody else can approve it, an admin included. It guards against an agent’s mistake or a prompt injection. It does not guard against you. In Terminal 2, write the rule and activate it.cat > confirm-git-push.yaml <<'EOF' apiVersion: straza.dev/v1beta1 kind: PolicySet metadata: name: confirm-git-push spec: rules: - id: confirm-git-push tools: [shell.exec] command: allowPatterns: ["git push*"] effect: allow mode: confirm EOF strazactl policy apply -f confirm-git-push.yaml strazactl policy activate confirm-git-pushYou should see
applied confirm-git-push (Off, …), thenpublished confirm-git-push, it is live nowwith the new snapshot.Recorded output
applied confirm-git-push (Off, 01a19de0-c0bc-7823-969e-caa9fb285ebc) published confirm-git-push, it is live now; new snapshot efb029c325312d809e3fb89379a34283b6d492d45aed4d8500e79df3456dd5b9The running Claude Code session uses the new rule within seconds. For a person with no sponsor, a rule with the mode
approvethat names no approver role holds the call the same way, because the request then goes to that person.Turn on approvals in this browser
Open
http://127.0.0.1:8420/self-service/. It saysSigned in as admin, because it shares the console’s sign-in. Press Sign out, then Sign in, and sign in as alice.Open This browser, press Enable this browser, keep or change the name, and press Enable. The browser makes a key that never leaves it. The server keeps only the public half. Use a normal window, since a private window deletes the key when it closes.
You should see
Enabled. This browser decides as alice., and one device row for alice markedthis browserandsoftware key.Approve your agent’s call
Ask Claude Code to run
git push. The hook denies it at once. Claude Code shows a reason that startsStraza: this call needs the requester's confirmationand ends with a reference.On the self-service page, open Requests. Within a few seconds a row shows
git pushfor alice, withonly the requester confirms. Press Approve, then Approve request. The dialog says the call runs now, but with Claude Code it runs when the agent sends it again.Within 60 seconds of your approval, ask Claude Code to run
git pushagain in the same session.You should see
Approved. git push runs now for alice.in the browser, and Claude Code runs the command on its next try. A try after that needs a new approval.The same calls without Claude Code
Send this in Terminal 4 for the deny and its reference. After you approve in the browser, send it again for the allow.
printf '%s' '{"hook_event_name":"PreToolUse","session_id":"laptop-1","cwd":"/tmp","tool_name":"Bash","tool_input":{"command":"git push"}}' | straza hook --harness claude-code; echo "exit $?"Straza: this call needs the requester's confirmation, on their enrolled phone, or the self-service page under This browser (http://127.0.0.1:18420/self-service/); retry after it is confirmed (ref 01a19de1-3f04-73a3-8a89-46c524213da7) {"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"Straza: this call needs the requester's confirmation, on their enrolled phone, or the self-service page under This browser (http://127.0.0.1:18420/self-service/); retry after it is confirmed (ref 01a19de1-3f04-73a3-8a89-46c524213da7)"}} exit 2One second after the approval, the same command was allowed.
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow"}} exit 0A third send got a new reference,
01a19de1-76cc-7543-991c-80ddacfc5eff, because the approval had been used.If this fails
- No row under Requests, or the row is gone
- A request waits 90 seconds for your approval and then expires. Ask Claude Code to run the command again, which opens a new request.
Read the record
Every step above left a record on the audit chain, where each record is linked to the one before it by a hash. In Terminal 2, read the newest five and check the chain.
strazactl audit tail --limit 5 strazactl audit verifyYou should see
The request, the deny with its reference, alice’s approval from her browser, its use, and the allowed
git pushwithconfirmed by alice. Thenaudit chain intact:and a count.Recorded output
Each record is trimmed. The real ones also carry the ids of the session, the user, the snapshot and the record, plus the source and the time.
#14 [alice] {"data":{"approvalId":"01a19de1-3f04-73a3-8a89-46c524213da7","lane":"hook","phase":"request","rule":"confirm-git-push","state":"pending","summary":"shell.exec: git push"},"type":"straza.audit.approval"} #15 [alice] {"data":{"command":"git push","effect":"deny","event":"tool.pre","harness":"claude-code","ruleId":"confirm-git-push","tool":"shell.exec"},"type":"straza.audit.tool"} #16 [alice] {"data":{"approvalId":"01a19de1-3f04-73a3-8a89-46c524213da7","channel":"browser","decidedDeviceId":"apd_01a19de1-0cd4-7200-9af5-ca482bc3d1a2","lane":"hook","phase":"resolution","state":"approved"},"type":"straza.audit.approval"} #17 [alice] {"data":{"approvalId":"01a19de1-3f04-73a3-8a89-46c524213da7","consumedAt":"2026-11-02T20:09:09Z","lane":"hook","phase":"consumed","state":"approved"},"type":"straza.audit.approval"} #18 [alice] {"data":{"command":"git push","effect":"allow","event":"tool.pre","harness":"claude-code","reason":"Straza: confirmed by alice (ref 01a19de1-3f04-73a3-8a89-46c524213da7)","ruleId":"confirm-git-push","tool":"shell.exec"},"type":"straza.audit.tool"} audit chain intact: 18 records verifiedWhen you are done, find the token’s id with
strazactl api-token listand revoke it withstrazactl api-token revokeand that id.Remove Straza from this machine
Remove the hooks first, while the service still runs. If the service stops while the hooks are installed, Claude Code can run no tool, because the hook denies every call with
Straza's background service for STRAZA_HOME … isn't running.straza uninstall claude-codeYou should see
removed Straza hooks for claude-codeandremoved Straza MCP server for claude-code, each with the file it changed.Then press Ctrl+C in Terminal 3 to stop the service and in Terminal 1 to stop the server. Delete alice’s key and the service’s state, and the directory Claude Code worked in.
rm -rf "$HOME/.straza" ~/straza-tryLast, delete the directory you started the server in. It holds the server’s data, the users and the admin token included.
What this protects on one laptop
The confirm rule catches a mistake by the agent, and a prompt injection that asks for a command you did not mean. The agent cannot approve its own call, because a session that a coding tool checked in can never decide a request.
It does not stop you, because you are the one who approves. A hook installed for your user only advises, so an agent that can run any command can bypass it. Started by hand, the service runs as you, so any program you run can read alice’s key, including the agent. An agent that can read your sign-in could also enroll an approval device of its own. Known limits has a row for each of the last three, and The security model shows where Straza enforces a boundary.
How this page was walked
Every terminal step ran in a fresh Alpine 3.20 container on Linux amd64, as an ordinary user, with the binaries built at v1.0.0-1700-g89d0cf1d. The browser steps ran in headless Chromium on the host.
For the host’s browser to reach the server, strazad ran with --listen :8420, so it listened on every address of the container. The container’s port 8420 was published on the host as 127.0.0.1:18420, and STRAZA_PUBLIC_URL was set to http://127.0.0.1:18420, so sign-in pages and links named the address the browser could reach. A socat forward made that address answer inside the container too. Every command therefore used http://127.0.0.1:18420 where this page says http://127.0.0.1:8420.
The container has no display, so straza enroll ran with --use-device-code, and alice signed in on the code page in the browser. straza install claude-code and Claude Code itself did not run. Each call of the agent went to straza hook --harness claude-code by hand, as the folds above show. The admin API token was minted in the console and reached strazactl through the read line as written.
The server start with the phone approver off and the removal steps ran on 2026-11-03 in a fresh container of the same kind, with the binaries built at v1.0.0-1709-gacba8249. With STRAZA_APPROVER_TLS_AUTO_MINT=false, nothing listened on port 8443, and port 8420 still served the approver routes and the self-service page. straza install claude-code and straza uninstall claude-code ran for the container’s own user. With the service stopped, git status sent to straza hook came back denied with exit status 2.
In an earlier walk on the same build, a retry 62 seconds after the approval asked again, and an approve rule with no approver role held alice’s call as her own confirm request.
Walked on v1.0.0-1700-g89d0cf1d on 2026-11-02, except Claude Code itself, whose calls went to straza hook by hand, and the enroll browser sign-in, done by device code.
Walked in a fresh Alpine 3.20 container on Linux amd64, with the browser steps in headless Chromium.