.md

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.

On this page

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 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.

Modes#

ModeReadsWritesWrites with confirm: trueWrites with confirm: "always"
manualaskaskaskask
auto (default)runrunaskask
runrunrunrunask

Use manual for a new pack or sensitive work, auto for daily work and run for unattended jobs such as CI. RFC 0006 proposes access, and the reference runtime and hcpx implement it.

Terminal
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:

Terminal
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" }
}
approvalsAn 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>
humanOnly a person running hcpx approve <id>. confirm=true from an agent does not count
Terminal
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#

LayerFileMay
User$HCP_HOME/access.json (~/.hcp/access.json)Loosen or tighten
EnvironmentHCP_MODE, HCP_APPROVALSLoosen or tighten, for one process
Project.hcp/access.json in the repositoryOnly 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.

~/.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:

kindFieldsDrawn as
messagefrom, to[], cc[], channel, subject, bodyA letter
paymentamount, currency, to, from, memoThe figure first
eventaction (create, update, cancel), title, when, until, where, attendees[]An event slip; a cancel is struck through
removalwhat, id, count, irreversibleWhat goes, and whether it can come back
publishtarget, what, versionWhere it goes
recordaction, entity, id, other fieldsThe record
fieldsanyRuled 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:

Terminal
hcpx preview ledger transfer -- amount=2.80 payee=demo-transit
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 proposes them.

kindMeansOffered
allow_onceRun this call, exactly as shown, onceAlways
reject_onceDo not run itAlways
allow_sessionRun it now, and let the same command run without asking for the rest of the sessionOnly 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 becauseThe 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 messageallow 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:

TypeScript
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:

CapabilityGatesTypical grant scope
pack.installEnabling a pack for a tenantAdmin
pack.connectEstablishing or refreshing Binding (apart from write)The account owner
verb.exec.readCommands with operation: readBroad
verb.exec.writeCommands with operation: writeNarrow; often per verb_key
sense.enableDelivering declared triggers into a sinkPer pack

The command's land decides which scope the runtime checks for the Principal:

LandRuntime must check
personalThe Principal's own scope
brainProject membership; tenant membership alone is not enough. project_id must be set
localThe 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.

TypeScript
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, 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:

TypeScript
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 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:

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.

Terminal
hcpx audit --limit 20
hcpx audit --pack ledger --json