Skip to content

Security ​

What to check before you point a flow at work you care about, and what holds an agent back while it works.

Nothing an agent does is put to you for approval

Every flow's agents run with approvals bypassed, whatever flow it is and whether or not you are watching. They edit files, run commands and make commits on their own. What holds an agent back is what its flow declares, below.

At a glance

  • You will make a run you can undo, read what each role of a flow may touch, and know which account every turn is billed to.
  • Use it when you are about to run a flow on a real repository, install a flow, or save an account.
  • You need Humanize installed, and git for the repository the flow works in.

Try it ​

The checklist, before a flow touches anything you care about:

  • The work can be undone. Run in a git repository and tag where you started (git tag start): git diff start then shows everything the flow changed, committed or not, and git reset --hard start takes it back.
  • You have read what each role may touch. It is in the flow's code, not in any menu.
  • You trust every flow you installed. Listing a flow runs its code.
  • You know which account each agent runs as. It is the account row of the agent, and every turn is billed to it.

How it works ​

A flow runs its agents unattended, so the protection has to be decided before the run, not during it. Three things decide it, and none of them is a prompt:

WhatDecided byWhere you check it
what each agent may read, write and reachthe flow, one role at a timethe flow's code, or its page under Flows
whose code runs at allyou, by installing a flow/flow, on its Installed page
whose credentials a turn usesyou, on the agent's account row/flow, and the Accounts page of /settings

Git is what makes the rest safe to try: a tag before the run turns everything the agents did into one diff you can read and one command that takes it back.

Example: read a flow's grants before running it ​

You want to run aot, which writes a flow from a description, in a repository you care about. First, make the run undoable:

sh
cd ~/src/app
git status --short             # nothing uncommitted of yours to lose
git tag start

Then read what its roles may touch. In hmz, open /flow. If aot is not installed yet, press Install more…, open official and install it, then come back to Installed. Put the cursor on aot, tab to Copy here, and press enter:

hmz · /flowdrawn, not recorded
  hmz › /flow › Installed
  …
  ╭────────────────────────────────────────────────────────────────────────╮
   official
   aot                                                               0.1.0
     Writes a flow from a description: drafted, loaded, smoke-run, revie…

   local 1
   @local/aot
     Writes a flow from a description: drafted, loaded, smoke-run, revie…
  ╰────────────────────────────────────────────────────────────────────────╯
   copied to .hmz/flows/aot -- you can edit it, and aot now points to it 2

    Install more…   Update   Uninstall    Copy here    Search…       Save

  enter copy   ←/→ move   tab list   esc back

Now the flow is a directory in your project, and its grants are one search away:

sh
grep -rn "_permission" .hmz/flows/aot
console
.hmz/flows/aot/__init__.py:51:    _permission = Permission(local=PermissionKind.READ, online=PermissionKind.NONE)

What to look at, by number:

  1. local appears under official: the copy is this project's own flow now, and no update to the installed aot changes what you run.
  2. aot now points to it: $aot and -f aot in this directory run your copy, which is the code you just read.

And in the search result: one role, the critic, declares local=READ and online=NONE. It reads the draft, never writes, and cannot reach the web. Every role that declares nothing, here all the others, runs at the default in the table below: it writes this directory and nothing else of yours.

Check it worked ​

  • git tag --list start prints start.
  • After the run, git diff start shows everything the flow changed, and nothing outside the repository was written by a role at the default grant.
  • On /flow, each role's account row reads the account you meant, as local or one of yours.

What a flow lets its agents touch ​

A flow declares a permission on each role it drives. You don't choose it at the prompt, so read it in the flow before you run the flow. It has four scopes:

ScopeWhat it coversA role that says nothing gets
localthe working directory of the sessionALL: read and write
userthe rest of the home directory the agent runs asREAD
systemeverything else on the machineREAD
onlinethe CLI's own web search and fetchALL

What that comes to in practice:

  • Every scope is enforced on the agent and on every command it runs, by Landlock on Linux and by Seatbelt on macOS. An agent at the default writes its working directory and nothing else of yours: its CLI's own settings, login and sessions, a temporary directory of its own, the shared /tmp and /var/tmp, the devices, and on Linux /proc. On macOS its login includes the login keychain, which is where Claude Code keeps it.
  • Claude Code, Codex, Grok Build, Qwen Code and pi never write your settings. Each process starts in a home of its own, made for it and removed when it stops: copies of the files the CLI rewrites as it runs (Claude Code's .claude.json, its backups and settings.json; Codex's and Grok Build's config.toml; Qwen Code's settings.json; pi's models.json and settings.json), links to the rest of yours, so its login and sessions are your own, and the flow's skills beside yours (Oh My Pi gets that home for the skills alone). What it writes into a copy, such as that it trusts the workspace, goes with it, and so does what Humanize adds there: Qwen Code's effort, hooks and tool server, a gateway account's model entry for Grok Build or provider for pi. Your system-wide settings and the project's own are read as the CLI always reads them.
  • user of ALL lets an agent change whatever your user account may change, anywhere on the machine, the file permissions being what hold it; run as root, the rest of the machine stays at system.
  • A read-only role (local of READ or NONE) also runs in its CLI's read-only mode.
  • online of NONE cuts the network, except the hosts the agent's model and login are at. The CLI's web tools are switched off too, where it can be told.
  • Work in a container or on an ssh host is fenced there too. Every command the agent runs on that machine is held by that machine's own Landlock or Seatbelt, around its own working directory and home.
  • A grant that cannot be held is refused, not widened. On a Linux kernel without Landlock, on a Mac where Humanize already runs inside another sandbox, and for work on a machine like either, a role runs only with ALL in every scope.
  • Some ways out remain. Neither Landlock nor Seatbelt governs Unix sockets, so an agent can still talk to a socket another program listens on, a docker daemon's among them; on macOS a system service reached over Mach is the same. With online of NONE on macOS, a program the agent runs may serve on any address, not only loopback. And whatever an agent can write, a later run can read.

Permissions lists what the official flows declare, and how each CLI holds to it.

Listing a flow runs its Python ​

A flow is a directory of Python, and Humanize runs it to find out what it is. Opening /flow, or naming a flow after $ at the prompt, runs every flow Humanize lists: the ones built in, the ones you installed, this project's and yours.

Installing a flow from a flowverse therefore trusts the repository it comes from with this machine, the way installing a package does: install only the flows you would install as one. Adding a flowverse runs nothing: it is an index of releases, and holds no code. official is always there: it is Humanize's own, at humanfia/flowverse, and its maintainers review every release it lists, which lowers the risk without removing it.

An installed flow is the exact commit its release names, and stays that commit until you update it: hmz fetches the flowverses each time it opens, and only says when a newer release is listed. To keep a flow as you read it whatever you install later, put the cursor on it in /flow and press Copy here, as in the example: it is copied into this project's own flows, and the flow's name here runs the copy.

Where your credentials are ​

An agent runs asHumanize keeps
as local, the defaultnothing. The CLI reads its own login, where it always keeps it.
a login made on the Accounts page of /settingsthe files that CLI wrote when it signed in, under ~/.hmz/providers/<cli>/<name>/
a key or a gateway made on the Accounts page of /settingswhat you typed, the key and a gateway's URL, in provider.json in that same directory
  • Those directories and files are readable by you alone.
  • The Accounts page of /settings names the variables an account sets and never shows their values. A secret you type is drawn as bullets.
  • A turn run as an account from /settings has the keys other accounts would use unset, so a key left in your shell profile cannot take its place.
  • Removing ~/.hmz removes every account. The CLIs' own logins stay where they are.

How an account's credentials reach a turn is in the Providers reference.

Other things worth knowing ​

  • Reporting. Humanize reports what goes wrong to its developers only if you said yes when it first asked. A report carries more than a crash; Reporting lists what is sent and what never is.
  • A remote target served over TCP is a shell on that machine for anyone who can reach the port. Give it a real token, or prefer ssh:// and docker://. See Remote execution.
  • /afk is about questions, not actions. It decides whether a flow may ask you something. It never makes an agent ask before it acts.
  • A dsh agent works with full access whatever its flow declares.
  • A project's hooks run only on the project's own machine. Project settings such as Claude Code's hooks or a .mcp.json stdio server start programs where the CLI runs. If a flow's agent runs on a different machine from its environment's work (harness local with the work elsewhere, or a harness runtime other than the work's machine), Humanize refuses to open the session while the project has any such setting. Run the agent on that machine (harness env), or remove the setting. See Remote execution.

Troubleshooting ​

A role is refused before the run starts ​

Its grant cannot be held here: on a kernel without Landlock, on a Mac where Humanize runs inside another sandbox, or on a machine like either, only a role with ALL in every scope runs. And a cursor-agent role with online of NONE is refused on every machine, because Cursor's web tools cannot be switched off. Run the flow on Linux with Landlock or on macOS outside any other sandbox, or choose another CLI for the role. See Permissions.

The flow changed things I did not expect ​

Read them all with git diff start, and take them back with git reset --hard start. Commits the agents made are undone too, since the tag is older. Files outside the repository are not in git: a role at the default grant cannot write there, but one granted user or system of ALL can.

A flow I read last week behaves differently ​

It was updated: an update installs the newer release over the one you read. Choose the release you read on the flow's page in /flow (Switch to), then Copy here to keep it as it is.

A turn was billed to the wrong account ​

The agent's account row decides it. Open /flow, enter on the role, and check account. An account that does not exist fails every turn of that agent, naming it; it never falls back to as local quietly.

Reporting a vulnerability ​

Report it privately, never in a public issue: SECURITY.md says how, what counts as a vulnerability, and when you will hear back.

Next steps ​

  • Permissions: what each official flow grants, and how each CLI holds it.
  • Accounts: API keys, gateways and second logins, kept apart.
  • Flowverses: what installing a flow means, and publishing your own.
  • Containers: put the work somewhere a mistake cannot reach you.

Released under the Apache-2.0 licence.