/** * Credential guard — layer 2 of specs/2026-10-04-agent-credential-isolation.md. * * One hook script (`config/hooks/credential-guard/guard.sh`) refuses an * agent's tool call that touches Crewly's credentials. This module writes * what the script reads and wires it into every runtime that has a pre-tool * hook: * * | Runtime | How | Per process? | * |---|---|---| * | Claude Code | PreToolUse group + `Read(...)` deny rules in the control-plane `--settings` file | yes | * | Codex | `-c hooks.PreToolUse=[...]` + `--dangerously-bypass-hook-trust` (probed) | yes | * | Gemini CLI | `GEMINI_CLI_SYSTEM_SETTINGS_PATH=` with a BeforeTool hook | yes | * | Antigravity (agy) | an entry in agy's global `~/.gemini/config/hooks.json` | no — the script ignores calls without CREWLY_SESSION_NAME, so the owner's own agy is unaffected | * | crewly-agent | its own tools run the script before reading or executing | yes | * | OpenCode | none (gap, documented) | — | * * @module services/agent/credential-guard.service */ import { type GuardedPath } from '../core/credential-files.js'; /** Formats the hook script speaks. */ export type CredentialGuardFormat = 'claude' | 'codex' | 'gemini' | 'antigravity'; /** What {@link prepareCredentialGuard} wrote. */ export interface CredentialGuardFiles { /** The paths file. */ pathsFile: string; /** The hook script (install root). */ script: string; /** One no-argument wrapper per format (`/hook-.sh`). */ wrappers: Record; /** Paths agents may not read. */ guarded: GuardedPath[]; } /** * Whether the guard is on for sessions launched by this backend. * * @param env - Backend environment * @returns false only when `CREWLY_CREDENTIAL_GUARD=0` */ export declare function isCredentialGuardEnabled(env?: NodeJS.ProcessEnv): boolean; /** * The paths-file body: the Crewly home, then each guarded path in absolute * form. The hook resolves every path argument it sees and compares it to * these; there are no tail or bare-name forms (a bare word like `cloud` * must never match). * * @param crewlyHome - CREWLY_HOME * @param guarded - Guarded paths * @returns File body */ export declare function buildCredentialGuardPathsBody(crewlyHome: string, guarded: GuardedPath[]): string; /** * The body of one per-format wrapper. * * Every wrapper fails OPEN: outside an agent session (no * CREWLY_SESSION_NAME — the owner's own runtime) or when the guard script is * gone (Crewly uninstalled or moved) it exits 0 without running anything. * That matters most for agy, which reads a non-zero exit (and `{}`) as * "deny": a missing script there would otherwise refuse every tool call in * every agy session, the owner's included. The agy wrapper therefore also * never passes the script's exit status through: it prints the script's * output only when it is a deny decision, and always exits 0. * * @param format - Hook format * @param script - The guard script * @param pathsFile - The paths file * @returns Wrapper body */ export declare function buildCredentialGuardWrapper(format: CredentialGuardFormat, script: string, pathsFile: string): string; /** * Write the paths file and one wrapper per format under * `/runtime/credential-guard/`. The wrappers take no arguments, * so a runtime that runs a hook command without a shell still works, and an * install path with spaces needs no quoting in TOML or JSON. * * @param crewlyHome - CREWLY_HOME * @param installRoot - Crewly install root (holds `config/hooks`) * @param guarded - Paths to guard (defaults to the credential inventory) * @returns What was written */ export declare function prepareCredentialGuard(crewlyHome: string, installRoot: string, guarded?: GuardedPath[]): CredentialGuardFiles; /** * Env vars an agent process gets so its hooks (and crewly-agent's own tools) * find the guard. * * @param files - Prepared guard * @returns Env map */ export declare function credentialGuardAgentEnv(files: Pick): Record; /** * {@link credentialGuardAgentEnv} from the fixed locations, without writing * anything (the launch path writes the files). Empty when the guard is off. * * @param crewlyHome - CREWLY_HOME * @param installRoot - Install root * @param env - Backend environment (kill switch) * @returns Env map */ export declare function credentialGuardEnvFor(crewlyHome: string, installRoot: string, env?: NodeJS.ProcessEnv): Record; /** Claude Code `Read(...)` deny rules for the guarded paths (built-in Read/Grep/Glob). */ export declare function claudeCredentialReadDenyRules(guarded: GuardedPath[]): string[]; /** * Codex launch arguments that attach the guard as a session PreToolUse hook. * * @param wrapper - The codex wrapper script (null: only the extra groups) * @param extraGroups - More TOML hook groups for the same list (the live-checkout guard) * @returns Arguments to insert after the `codex` command word, or null when * the wrapper path cannot be embedded safely */ export declare function codexCredentialGuardArgs(wrapper: string | null, extraGroups?: readonly string[]): string | null; /** * Insert the Codex guard arguments after the codex command word. * * @param command - Launch command * @param args - From {@link codexCredentialGuardArgs} * @returns The command, unchanged when it already has the trust flag */ export declare function withCodexCredentialGuard(command: string, args: string): string; /** * Write Crewly's Gemini CLI system settings file (BeforeTool hook), merged * over the machine's own system settings file so that one is not shadowed. * * @param dir - Runtime dir * @param wrapper - The gemini wrapper script * @param platform - Platform (for the default system settings path) * @returns The settings file path */ export declare function writeGeminiCredentialGuardSettings(dir: string, wrapper: string, platform?: NodeJS.Platform): string; /** What happened to agy's hooks file. */ export type AntigravityHookResult = 'written' | 'unchanged' | 'removed' | 'skipped-not-object'; /** * Add (or, with `wrapper` null, remove) Crewly's entry in agy's global hooks * file, keeping every other entry and the file's mode. A symlinked file is * written through to its target. A file that is not a JSON object, or a * dangling link, is left alone (`skipped-not-object`): agy sessions then run * unguarded, with a WARN at launch. * * @param wrapper - The antigravity wrapper, or null to remove the entry * @param homeDir - Home directory (agy resolves the file from $HOME) * @returns What was done */ export declare function syncAntigravityCredentialHook(wrapper: string | null, homeDir?: string): AntigravityHookResult; /** * The command agy runs for Crewly's hook entry. agy runs it through `sh` * (verified on 1.2.14) and treats any non-zero exit as deny, so the entry * itself fails open when the wrapper is gone (`~/.crewly` deleted, Crewly * uninstalled): `[ -f W ] || exit 0; exec bash W`. `test -f W && bash W` * would deny (exit 1) — the wrong form. * * @param wrapper - The antigravity wrapper * @returns Shell command string */ export declare function antigravityHookCommand(wrapper: string): string; //# sourceMappingURL=credential-guard.service.d.ts.map