---
title: "Access"
description: "Grants decide who may act at all, and access decides what runs without asking, through modes, rules, approvals, the approval card and an audit log."
canonical: https://harnesscontextprotocol.io/docs/concepts/access
---

# Access

> Grants decide who may act at all, and access decides what runs without asking, through modes, rules, approvals, the approval card and an audit log.

Access is the user's or operator's setting for what a runtime runs without
asking, and capability grants decide before that who may act at all. Packs set
neither (their [Policy](/docs/concepts/policy) only says what a command is), so
they cannot grant themselves anything. Pick a mode and a few rules for daily
work, and give a multi-tenant product real [grants](#authorization).

## Modes

| Mode | Reads | Writes | Writes with `confirm: true` | Writes with `confirm: "always"` |
|------|-------|--------|-----------------------------|----------------------------------|
| `manual` | ask | ask | ask | ask |
| `auto` (default) | run | run | ask | ask |
| `run` | run | run | run | ask |

Use `manual` for a new pack or sensitive work, `auto` for daily work and `run`
for unattended jobs such as CI.
[RFC 0006](https://github.com/harnesscontextprotocol/hcp-protocol/blob/master/rfcs/0006-access-modes.md)
proposes access, and the reference runtime and `hcpx` implement it.

```bash
hcpx mode                       # show the current mode and where it comes from
hcpx mode manual                # for everything
hcpx mode run --pack transit    # for one pack
HCP_MODE=run hcpx exec …        # for one process (CI)
```

## Confirm "always"

A command marked `"confirm": "always"` in `policy.json` asks in every mode,
including `run`. Use it for anything that moves money, deletes data for good or
reaches outside the company; `hcpx check` suggests it (rule P007) for commands
that pay or transfer. Only an allow rule a person wrote naming the command
exactly lets it run without a question, and a wildcard allow rule never does.

## Rules

Rules refine the mode for one command, a whole pack or everything:

```bash
hcpx allow ledger.transfer --when payee=demo-transit --max amount=5 --note "fares"
hcpx ask gh.*                          # always ask for anything in the gh pack
hcpx deny notes.delete                 # never, whatever the mode
hcpx deny '*' --op write               # read-only everywhere
hcpx rules                             # list, with index and scope
hcpx rules rm 0
```

`--when key=value` requires an argument to equal a value and `--max key=n` caps
a numeric one, and a rule whose conditions fail does not apply. A matching deny
rule refuses the command (`denied_by_rule`). Otherwise the most specific
matching ask or allow rule decides: an exact command beats `pack.*`, which beats
`*`, a rule with conditions beats one without, and ask wins a tie. With no
matching rule, the mode decides.

## Approvals

When a command asks, nothing runs. The result carries `code: "confirm_required"`,
a `preview` of the exact command and an `approval_id`:

```json
{
  "ok": false,
  "code": "confirm_required",
  "preview": { "command": "transfer", "args": { "amount": "2.80", "payee": "demo-transit" },
               "cli": "ledger transfer 2.80 --to demo-transit --confirm" },
  "approval_id": "5ea7eb29",
  "access": { "mode": "auto", "reason": "Policy confirm \"always\": every mode asks", "approvals": "agent" }
}
```

| `approvals` | An ask is satisfied by |
|-------------|------------------------|
| `agent` (default) | The agent passing `confirm=true`, after the user agreed in the conversation, or a person running `hcpx approve <id>` |
| `human` | Only a person running `hcpx approve <id>`. `confirm=true` from an agent does not count |

```bash
hcpx approvals            # requests waiting for a person
hcpx approve 5ea7eb29     # yes, once
hcpx reject 5ea7eb29
HCP_APPROVALS=human …    # or "approvals": "human" in access.json
```

An approval covers one run of exactly that command with exactly those arguments
for 15 minutes, so the agent reruns it unchanged. Changing any argument, even
the amount by a cent, needs a new approval.

## Settings and who may change them

| Layer | File | May |
|-------|------|-----|
| User | `$HCP_HOME/access.json` (`~/.hcp/access.json`) | Loosen or tighten |
| Environment | `HCP_MODE`, `HCP_APPROVALS` | Loosen or tighten, for one process |
| Project | `.hcp/access.json` in the repository | Only tighten: a stricter mode, ask and deny rules, human approvals |

The project file only tightens because anyone who can open a pull request can
write it. `hcpx access` prints the effective settings, where each comes from,
and anything ignored because it would loosen access.

```json title="~/.hcp/access.json"
{
  "mode": "auto",
  "packs": { "transit": { "mode": "run" } },
  "approvals": "human",
  "rules": [
    { "match": "ledger.transfer", "decision": "allow",
      "when": { "args": { "payee": "demo-transit" }, "max": { "amount": 5 } }, "note": "fares" },
    { "match": "notes.delete", "decision": "deny" }
  ]
}
```

## Keeping agents out of access

The access commands (`hcpx approve`, `reject`, `mode`, `allow`, `ask`, `deny`,
`rules rm`) refuse with `human_only` inside an agent, meaning through `hcpx mcp`
or with `HCP_AGENT=1` set. `hcpx adopt claude` sets that variable for Claude
Code's shell and adds deny rules in `.claude/settings.json` for those commands
and for edits to the HCP store, and `hcpx adopt codex` prints it for Codex's
`shell_environment_policy`.

This stops mistakes and prompt injection that go through HCP. On a single-user
machine the agent runs as you, so one with unrestricted access to your files can
still get past it; for a hard boundary, run the runtime under its own account or
as a service, or use a host sandbox that cannot write to the HCP store.

## What the card shows

A host shows a person a small typed view of what will happen, with the exact
command under it. The runtime builds the view from the pack and the call's real
arguments, never from text the model wrote, so an agent cannot show one thing
and run another. `res.approval.view` is one of:

| `kind` | Fields | Drawn as |
|--------|--------|----------|
| `message` | `from`, `to[]`, `cc[]`, `channel`, `subject`, `body` | A letter |
| `payment` | `amount`, `currency`, `to`, `from`, `memo` | The figure first |
| `event` | `action` (create, update, cancel), `title`, `when`, `until`, `where`, `attendees[]` | An event slip; a cancel is struck through |
| `removal` | `what`, `id`, `count`, `irreversible` | What goes, and whether it can come back |
| `publish` | `target`, `what`, `version` | Where it goes |
| `record` | `action`, `entity`, `id`, other fields | The record |
| `fields` | any | Ruled rows |

Every view also has a `title` in words ("Pay 2.80 EUR to demo-transit"), `rest`
for the arguments it did not use, shown as rows so nothing stays hidden, and
`signals`, the facts in the values a person should weigh, such as "Cannot be
undone", "Goes to 7 people", "Tells 3 people it is cancelled" or "Goes live".
The request's `reason_code` (`confirm_always`, `confirm`, `mode_manual`,
`rule_ask`) lets a UI word the reason without parsing sentences. `hcpx preview`
shows the card before anything runs:

```bash
hcpx preview ledger transfer -- amount=2.80 payee=demo-transit
```

```text
Ledger · Pay a saved payee. Refused until a person confirms the amount and payee.

Pay demo-transit
· Moves money
  amount    2.80
  currency  EUR
  to        demo-transit
  from      Demo account DEMO-0001

  $ ledger transfer 2.80 --to demo-transit --confirm
```

A pack can declare the view for a command in `surface.json`:

```json
"transfer": {
  "description": "Pay a saved payee. Refused until a person confirms the amount and payee.",
  "cli": ["ledger", "transfer", "{{amount}}", "--to", "{{payee}}", "--memo", "{{memo}}", "--confirm"],
  "preview": {
    "kind": "payment",
    "title": "Pay {{payee}}",
    "fields": { "amount": "{{amount}}", "currency": "EUR", "to": "{{payee}}", "from": "Demo account DEMO-0001", "memo": "{{memo}}" }
  }
}
```

Without a `preview`, the runtime infers the kind from the command and argument
names: `send` with `to` and `subject` is a message, `transfer` with `amount` a
payment, `calendar cancel` an event, `remove` a removal. The inference and the
signals are data (`DEFAULT_VIEW_RULES`, `DEFAULT_SIGNAL_RULES`), and a host adds
its own ahead of them with `Hcp.create({ views: { rules, signals } })`. A
declared `preview` always wins, so declare one for the commands that matter.

### Command lines

Hosts that run commands as text, such as an agent's shell line, parse it with
`argsFromArgv(pack, command, argv)`, where `argv` is what follows the command's
own words. The declared arguments decide the parse: `--node-id=n-7` and
`--node-id n-7` both fill `node_id`, `{{placeholders}}` in `cli[]` take their
positions, an argument with `"x-position": 0` takes the first bare word, and
undeclared `--switches` stay switches.

Declaring `args` for every command that asks keeps the card exact. `hcpx check`
warns (M024) when a preview names an unknown kind or a placeholder that is not an
argument, and suggests the exact `preview` JSON (M025) for a confirm command
without one, which `hcpx wrap` writes into the draft for every command it marks
`confirm`. Because the card is built from their arguments, the HCP Skill tells
agents to pass the whole action in named arguments, one risky command per call.

## Answers a host can offer

Every approval request carries `options`, the answers a host may put next to the
card. The runtime computes them, and the host sends back only the kind a person
picked, never a rule of its own.
[RFC 0007](https://github.com/harnesscontextprotocol/hcp-protocol/blob/master/rfcs/0007-host-projection.md)
proposes them.

| `kind` | Means | Offered |
|--------|-------|---------|
| `allow_once` | Run this call, exactly as shown, once | Always |
| `reject_once` | Do not run it | Always |
| `allow_session` | Run it now, and let the same command run without asking for the rest of the session | Only where a lasting yes cannot widen what the person saw |

`allow_session` carries the `rule` it stands for, an allow rule with
`note: "session"`:

| The command asks because | The rule |
|--------------------------|----------|
| The mode or an ask rule (no `confirm` in its Policy) | `allow` on `<pack>.<command>` |
| Its Policy says `confirm`, and the card is a `message` | `allow` on `<pack>.<command>` only while the arguments the card took `to`, `cc`, and `channel` from keep the same values; `about` names them in words |

No `confirm: "always"` command gets `allow_session`, and neither does a removal,
payment, publish or event card, because each of those calls is its own decision.
The view's `sources` name the arguments behind each field, which lets a message
rule bind the audience the person saw. The SDK computes the options and the rule
but stores no session rules: a host that offers `allow_session` keeps the rule
in its access settings until the session ends, never lets the agent set one, and
still lets a deny rule win over it.

## In your own runtime

Any runtime gets access, with or without the CLI, from settings passed to
`Hcp.create` or by wrapping its driver:

```ts
import { Hcp, withAccess, gateCommand } from '@harnesscontextprotocol/runtime'

// A proprietary driver gets modes, rules, approvals, and audit in one line.
const hcp = await Hcp.create({
  runtime: myDriver,
  access: { mode: 'auto', approvals: 'human' },
  audit: (event) => log.write(event),
})

const res = await hcp.exec('ledger', 'transfer', { amount: '2.80', payee: 'demo-transit' })
if (res.code === 'confirm_required') {
  showApprovalCard(res.approval)          // pack, summary, arguments with descriptions, CLI line, why
}

// After the person presses Approve in your UI:
await hcp.exec('ledger', 'transfer', { amount: '2.80', payee: 'demo-transit' }, {
  approval: { by: 'human', id: uiApprovalId },
})
```

`res.approval` carries everything a host needs to ask a person: `pack_name`,
`command`, `summary`, `operation`, `confirm`, `args` (each with its
`description` from the pack), `cli`, `reason`, `reason_code`, `mode`,
`approvals`, the `view`, the `options` and, when the runtime keeps approvals,
`approval_id` and `expires_at`. A host that runs commands its own way, for
example from a command line an agent wrote, calls
`gateCommand({ pack, verb_key, args, access })` and gets allow, ask (with the
same request) or deny. `approval: { by: 'human' }` is the host's statement that
its own UI collected a person's yes for exactly this call, so agents never set
it.

`hcpx commands` shows each command's `access` (allow, ask, deny) under the
current settings, so an agent plans around an ask before it hits one. The Skill
tells agents to follow the `hint` of a `confirm_required` result, never to run
the access commands, and to stop on `denied_by_rule`.

## Authorization

Grants decide whether a Principal may cause an effect at all. The runtime grants
through its own RBAC, and packs never embed user allow-lists, role names or API
keys, so the same pack runs in a local demo and in a multi-tenant product. A
runtime grants at least these five capabilities, per `pack_id` or per
`verb_key`, and a multi-tenant runtime must not collapse read, write and connect
into one boolean:

| Capability | Gates | Typical grant scope |
|------------|-------|---------------------|
| `pack.install` | Enabling a pack for a tenant | Admin |
| `pack.connect` | Establishing or refreshing Binding ([apart from write](/docs/concepts/binding#connect)) | The account owner |
| `verb.exec.read` | Commands with `operation: read` | Broad |
| `verb.exec.write` | Commands with `operation: write` | Narrow; often per `verb_key` |
| `sense.enable` | Delivering declared triggers into a sink | Per pack |

The command's land decides which scope the runtime checks for the
[Principal](/docs/concepts/binding#principal):

| Land | Runtime must check |
|------|--------------------|
| `personal` | The Principal's own scope |
| `brain` | Project membership; tenant membership alone is not enough. `project_id` must be set |
| `local` | The sandbox working directory. Not a tenant |

The reference SDK's grant shape is what its default authoriser understands, and
HCP does not require it as a wire format. If you omit `grants`, `Hcp.create`
falls back to `permissiveGrants()`, which is fine for a local demo and wrong for
anything multi-tenant.

```ts
type HcpGrant = {
  capability: 'pack.install' | 'pack.connect' | 'verb.exec.read' | 'verb.exec.write' | 'sense.enable'
  pack_id?: string        // omit → any pack
  verb_keys?: string[]    // omit → any command in the pack
}
```

```json
[
  { "capability": "verb.exec.read",  "pack_id": "mail" },
  { "capability": "verb.exec.write", "pack_id": "mail", "verb_keys": ["mail.send"] },
  { "capability": "pack.connect",    "pack_id": "mail" }
]
```

The authoriser runs inside the [exec pipeline](/docs/runtimes/execution#the-pipeline),
after the Principal's scope is resolved and before the confirm step where modes,
rules and approvals apply. Pass `authorize` to the runtime driver to replace the
default:

```ts
import { defaultAuthorize } from '@harnesscontextprotocol/sdk'

defaultAuthorize(
  { principal: { id: 'u1', kind: 'human' }, tenant_id: 't1', project_id: null, grants },
  { pack_id: 'mail', verb_key: 'mail.send', operation: 'write', land: 'personal', kind: 'exec' },
)
// { allow: true } | { allow: false, reason: 'missing verb.exec.write for mail.mail.send' }
```

[Errors](/docs/reference/errors#authorization) lists every denial reason. Role
table schemas, row-level security policies, collection names, IdP vendor APIs
and host paths belong to adopter profiles, since the protocol fixes only the
five axes and the evaluation order.

## Audit

Every write is attributed with this tuple, and reads should be too:

```text
Principal + Tenant (+ Project if land = brain) + Binding identity + pack_id + verb_key + operation
```

The reference runtime writes each decision as one line in
`$HCP_HOME/audit.jsonl`: time, Principal, project, pack, command, read or write,
the decision and its reason, the mode, who approved (agent, human with approval
id, rule, or mode), whether it ran, and a SHA-256 of the arguments. The line
keeps argument names and never their values.

```bash
hcpx audit --limit 20
hcpx audit --pack ledger --json
```

## Related

- [Error codes](/docs/reference/errors#error-codes)
- [Specification: Authorization](/docs/specification/authorization)

## Sitemap

- [Documentation](https://harnesscontextprotocol.io/docs)
- [Packs](https://harnesscontextprotocol.io/packs)
- [Glossary](https://harnesscontextprotocol.io/docs/reference/glossary)
- [llms.txt](https://harnesscontextprotocol.io/llms.txt)
- [llms-full.txt](https://harnesscontextprotocol.io/llms-full.txt)
- [OpenAPI](https://harnesscontextprotocol.io/openapi.json)
- [MCP](https://harnesscontextprotocol.io/mcp)
