Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 30 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -335,31 +335,38 @@ Such a client is then pointed at the gateway with `HTTPS_PROXY` and
- **A machine-local certificate authority is generated** under
`~/.hyp/hypaware/tls`, readable only by you, and name-constrained so it
cannot vouch for any host outside the provider set HypAware intercepts.
On macOS it can also be added to your **login keychain** as a user-domain
trusted root, for a client whose transport trusts only the keychain: macOS
raises its own password dialog, and declining it leaves capture working
with that inbound channel off. No admin rights are needed and the
machine-wide system keychain is not touched. On other platforms trust
stays file-scoped to the client's own settings. `hyp status` shows the
fingerprint, every host the CA is permitted to vouch for, and whether the
keychain still trusts it. `hyp client detach <client>` keeps the CA and the
trust, so re-attaching does not ask again; `hyp client detach <client> --purge`
and `hyp daemon uninstall` remove both.
- **On macOS, a proxy attach also leaves a login-session variable behind.**
Bun picks its trust store before any settings file is read, so a keychain
root only counts if `NODE_USE_SYSTEM_CA=1` is already in the process
environment. The attach that trusted the CA therefore ran `launchctl
setenv NODE_USE_SYSTEM_CA 1` and installed a small LaunchAgent,
Trust stays file-scoped to the proxied client's own settings: nothing
installs it into any OS trust store, including your login keychain, and
anything wider is your own decision. Earlier releases attached Claude
Code by proxy and did add the CA to the macOS **login keychain** as a
user-domain trusted root, for a transport that trusts only the keychain;
macOS raised its own password dialog for that, it never needed admin
rights, and the machine-wide system keychain was never touched. If you
ran one of those releases, that trust setting is still on your account
until you remove it. `hyp status` shows
the fingerprint, every host the CA is permitted to vouch for, and whether
the login keychain still trusts it. `hyp client detach <client>` leaves the CA
and any trust an earlier release was granted in place, because a detach is
not a statement about the certificate and no attach re-creates the grant;
`hyp client detach <client> --purge` and `hyp daemon uninstall` remove both.
- **On macOS, an earlier proxy attach also left a login-session variable
behind.** Bun picks its trust store before any settings file is read, so a
keychain root only counts if `NODE_USE_SYSTEM_CA=1` is already in the
process environment. The attach that trusted the CA therefore ran
`launchctl setenv NODE_USE_SYSTEM_CA 1` and installed a small LaunchAgent,
`~/Library/LaunchAgents/com.hyperparam.hypaware.node-system-ca.plist`,
whose only job is to re-run that command at each login. It stays a login
item on your machine until it is removed, and it is a session-wide
variable that other Node programs read too. `launchctl setenv` reaches
whose only job is to re-run that command at each login. No attach writes
either one today; on a machine that ran one of those releases the agent
stays a login item until it is removed, and the variable stays session-wide
for other Node programs to read too. `launchctl setenv` reaches
processes launched after it, so a terminal app that was already running
must be fully quit and reopened. `hyp client detach <client>` unsets the
variable and removes the agent, as do `hyp client detach <client> --purge` and
`hyp daemon uninstall`; `hyp client attach claude` unwinds it when it migrates a
previously proxied machine; and `hyp status` shows whether the variable is
currently live.
must be fully quit and reopened. `hyp client detach <client> --purge` and
`hyp daemon uninstall` clear both unconditionally, and `hyp client attach claude`
unwinds them when it migrates a previously proxied machine. A plain
`hyp client detach <client>` only clears them while that client's attach marker still
records a proxy attach, so on a machine already migrated to another attach
mode it is not the command that removes the leftover. `hyp status` shows
whether the variable is currently live.
- **Only the hosts a registered upstream names are decrypted.** Every other
host the client talks to is tunnelled through without being decrypted.
- **What gets recorded does not change.** Only the recorded API paths are
Expand Down
53 changes: 29 additions & 24 deletions docs/PRIVACY.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,37 +69,42 @@ outside the provider set HypAware's client adapters intercept (today
`api.anthropic.com`, `api.openai.com`, `chatgpt.com`). All IP addresses are
excluded.

**Where it is trusted.** On macOS, attach installs the CA into your **login
keychain** as a user-domain trusted root, because Claude Code's Remote
Control transport trusts only the keychain and nothing else. This does
change your account's certificate trust settings, which is why macOS itself
raises the password dialog: an application running as you that consults the
**Where it is trusted.** Trust stays file-scoped to the proxied client's own
settings: nothing HypAware runs installs the CA into an OS trust store,
including your login keychain, and anything wider is your own decision.
Earlier releases attached Claude Code by proxy and did install it into your
**login keychain** as a user-domain trusted root, because that client's
Remote Control transport trusted only the keychain and nothing else. That
changed your account's certificate trust settings, which is why macOS itself
raised the password dialog: an application running as you that consults the
login keychain will accept certificates this CA signs, for those hosts.
Declining the dialog is supported and capture keeps working without it, with
only Remote Control's inbound channel lost. The change never needs admin
rights, and the machine-wide system keychain and other user accounts are
never modified. On other platforms the CA is trusted only by Claude Code,
through that client's own settings.

**What else macOS attach leaves behind.** The keychain root only takes
effect if `NODE_USE_SYSTEM_CA=1` is in the environment before Claude Code
starts, so attach also runs `launchctl setenv NODE_USE_SYSTEM_CA 1` and
installs a LaunchAgent at
Declining the dialog was supported and capture kept working without it, with
only Remote Control's inbound channel lost. The change never needed admin
rights, and the machine-wide system keychain and other user accounts were
never modified. If you ran one of those releases, that trust setting is
still on your account until you remove it.

**What else an earlier macOS attach left behind.** The keychain root only
took effect if `NODE_USE_SYSTEM_CA=1` was in the environment before Claude
Code started, so that attach also ran `launchctl setenv NODE_USE_SYSTEM_CA 1`
and installed a LaunchAgent at
`~/Library/LaunchAgents/com.hyperparam.hypaware.node-system-ca.plist` that
re-runs that one command at each login. What it runs is `/bin/launchctl`
itself, once, which sets the variable and exits: there is no resident
process, no HypAware code in it, and nothing is sent anywhere. It is still a
login item on your machine, and a session-wide variable that other Node
programs will also read. `hyp client detach claude` unsets the variable and
removes the agent, as do `hyp client detach claude --purge` and
`hyp daemon uninstall`.
process, no HypAware code in it, and nothing is sent anywhere. No attach
writes either one today. On a machine that ran one of those releases it is
still a login item, and still a session-wide variable that other Node
programs will also read.

**Its lifetime.** `hyp status` shows the fingerprint, every host the CA is
permitted to vouch for, whether the keychain still trusts it, and whether
the launchd variable is live. `hyp client detach claude` deliberately keeps
the CA and the trust in place, so re-attaching later does not ask for your
password again; `hyp client detach claude --purge` and `hyp daemon uninstall`
remove the CA and its keychain trust.
the launchd variable is live. `hyp client detach claude --purge` and `hyp daemon
uninstall` remove the CA, its keychain trust, the launchd variable, and the
login agent. A plain `hyp client detach claude` leaves the CA and any trust an
earlier release was granted in place, because a detach is not a statement
about the certificate and no attach re-creates the grant; it clears the
launchd variable and its agent only while that client's attach marker still
records a proxy attach.

## Where it goes

Expand Down
19 changes: 13 additions & 6 deletions hypaware-core/plugins-workspace/claude/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -550,12 +550,19 @@ async function runClaudeAndOtelLocalPreset(argv, ctx) {
// @ref LLP 0114#init-writes-no-listen [implements]: the preset leaves listen unset so the default install keeps its fallback
{
name: '@hypaware/ai-gateway',
// Claude still requires the gateway capability as its normalized
// exchange writer, but its OTEL attach sends no model traffic through
// the gateway and therefore composes neither proxy mode nor an
// Anthropic upstream.
// @ref LLP 0262#capture [implements]: OTEL is the producer and the gateway capability remains the projection seam
config: { upstreams: [] },
config: {
// No `proxy_mode`: this preset writes literally what the picker fold
// composes, and the claude row stopped declaring
// `gateway_proxy_mode` when its attach became otel-only. Writing the
// key here would mint a CA on first boot that nothing in this
// install ever presents, and hand `hyp status` a proxy-trust block
// reporting keychain and launchd state no attach on this machine
// can ever change.
// Claude still requires the gateway capability as its normalized
// exchange writer, but OTEL sends no model traffic through it.
// @ref LLP 0262#capture [implements]: OTEL is the producer and the gateway capability remains the projection seam
upstreams: [],
},
},
{
name: '@hypaware/otel',
Expand Down
8 changes: 6 additions & 2 deletions hypaware-core/smoke/flows/claude_attach_detach.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,10 +157,14 @@ export async function run({ harness, expect }) {
}
)
expect.that('dispatch: hyp client attach claude exited 0', attachCode, (v) => v === 0)
// Silent stderr: the claude row stopped declaring proxy attach when the
// client went otel-only, so there is no migration to point a scripted
// attach at, and the LLP 0244 pointer is gone with it.
// @ref LLP 0262#migration [tests]: an otel attach offers no proxy-mode switch, in any shape
expect.that(
'stderr: hyp client attach had no errors',
'stderr: hyp client attach had no errors and no proxy-mode note',
attachStderr.text(),
(v) => typeof v === 'string' && v.length === 0
(v) => v === ''
)
expect.that(
'stdout: hyp client attach printed the settings path',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,10 @@ async function goldenPickerConfig(hypHome) {
{ name: 'openai', base_url: 'https://api.openai.com', path_prefix: '/v1', provider: 'openai' },
{ name: 'chatgpt', base_url: 'https://chatgpt.com', path_prefix: '/backend-api/codex', provider: 'chatgpt' },
],
// No `proxy_mode`: no bundled picker row declares proxy attach since
// the claude client went otel-only, so the wizard composes a gateway
// that mints no CA.
// @ref LLP 0262#requirements [tests]: R5 - a composed claude install needs no CA and no keychain trust
},
},
{
Expand Down
7 changes: 4 additions & 3 deletions src/core/cli/core_commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -401,9 +401,10 @@ function buildCoreCommands(registry) {
'',
'Detaching stops future capture for that client; it does not delete',
'anything already recorded (see hyp privacy purge for that).',
'A proxy-mode detach keeps the local interception CA and its keychain',
'trust so a later re-attach needs no new password dialog; --purge',
'removes both as well.',
'A detach keeps the local interception CA, and any OS trust store',
'grant an earlier release was given: no attach re-creates that grant,',
'so it is a leftover rather than a convenience being held for you.',
'--purge removes the CA and that trust as well.',
'--dry-run reports what would change without writing.',
].join('\n'),
aliases: ['detach', 'unattach'],
Expand Down
21 changes: 17 additions & 4 deletions src/core/commands/clients.js
Original file line number Diff line number Diff line change
Expand Up @@ -901,19 +901,31 @@ async function maybeInteractiveEnableAttach({ name, ctx, parsed, enablement }) {
* config write is still the repair the gateway's own stale-CA warning asks
* for.
*
* No bundled picker row declares `gateway_proxy_mode` since LLP 0262: the
* `claude` row was the only one, and dropped it when its attach became
* otel-only. The rule stays generic for any client still captured by the
* proxy (LLP 0243's composition rule is unchanged); what it must never do is
* fire for a client whose attach repoints no base URL, because every clause
* of the question below would then be false.
*
* Never throws into the attach: the caller downgrades any escape to a
* warning, because base-URL attach is what this install already does and
* remains the working fallback.
*
* @ref LLP 0244#attach-offers [implements]: one consented question, default no, naming the config write, the restart, and the coming trust dialog
* The question names the config write and the restart, and says where the CA
* is trusted. It no longer promises a macOS trust dialog: no production path
* calls the trust-store writer since the claude attach went otel-only, so a
* yes mints the CA and stops there.
*
* @ref LLP 0244#attach-offers [implements]: one consented question, default no, naming the config write and the restart
* @ref LLP 0244#central-managed [implements]: a fleet-owned gateway block reports instead of prompting
* @ref LLP 0244#non-interactive [implements]: non-TTY and --json attaches never migrate; they emit the one-line pointer
* @param {{ name: string, ctx: CommandRunContext, parsed: { client: string, dryRun: boolean, json: boolean } }} args
* @returns {Promise<void>}
*/
async function maybeOfferProxyModeMigration({ name, ctx, parsed }) {
// A dry run changes nothing and promises nothing, so it says nothing.
// `hyp attach all` never prompts mid-run either (same posture as
// `hyp client attach all` never prompts mid-run either (same posture as
// maybeInteractiveEnableAttach above), but it does not return here: it
// falls through to the one-line pointer below, because LLP 0244
// #non-interactive owes every non-migrating attach shape the line naming
Expand Down Expand Up @@ -997,8 +1009,9 @@ async function maybeOfferProxyModeMigration({ name, ctx, parsed }) {
ctx,
`${capitalizeClientLabel(name)} can attach through HypAware's local HTTPS proxy instead of a ` +
`repointed base URL, which keeps Remote Control working. Switching writes proxy_mode ` +
`into the local config and restarts the daemon; macOS will then ask to trust the ` +
`HypAware Local CA. Switch this install to proxy mode now? [y/N] `
`into the local config and restarts the daemon, which mints the HypAware Local CA; ` +
`nothing adds that CA to a system trust store, so the client trusts it through its own ` +
`settings. Switch this install to proxy mode now? [y/N] `
)
if (!accepted) {
ctx.stderr.write(
Expand Down
Loading
Loading