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
6 changes: 6 additions & 0 deletions cmd/onecli/hook_gateway_detect.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/bash
# OneCLI gateway detection hook for Claude Code.
# Injected by `onecli run` — outputs context only when the gateway proxy is active.
if echo "$HTTPS_PROXY" | grep -q "aoc_"; then
echo "OneCLI gateway is active. Load /onecli-gateway for any external service access (email, calendar, GitHub, Slack, Stripe, databases, APIs). Never use browser automation or MCP auth flows."
fi
83 changes: 82 additions & 1 deletion cmd/onecli/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ import (
//go:embed skill_gateway_fallback.md
var gatewaySkillFallback string

//go:embed hook_gateway_detect.sh
var gatewayDetectHook string

// RunCmd is `onecli run -- <command> [args...]`.
type RunCmd struct {
Project string `optional:"" short:"p" help:"Project slug."`
Expand Down Expand Up @@ -104,14 +107,15 @@ func (c *RunCmd) Run(out *output.Writer) error {
// Build child environment.
env := buildChildEnv(os.Environ(), cfg.Env, caPath)

// Install skill for known agents (silently updates stale files).
// Install skill and hook for known agents (silently updates stale files).
// Fetch the latest skill from the API; fall back to the embedded copy.
if name, dir, cfgDir, ok := agentSkillDir(c.Args[0]); ok {
skillContent := gatewaySkillFallback
if fetched, err := client.GetGatewaySkill(newContext()); err == nil && fetched != "" {
skillContent = fetched
}
maybeInstallGatewaySkill(out, name, dir, skillContent)
maybeInstallGatewayHook(out, name, dir)

// Electron-based agents (e.g. Cursor) ignore embedded user:pass in
// HTTPS_PROXY and show a native auth dialog. Inject proxy credentials
Expand Down Expand Up @@ -362,6 +366,83 @@ func maybeInstallGatewaySkill(out *output.Writer, agentName, baseDir, content st
out.Stderr(fmt.Sprintf("onecli: installed gateway skill for %s.", agentName))
}

// maybeInstallGatewayHook installs the gateway detection hook script and
// registers it in the agent's settings.json so the agent knows the gateway
// is active without needing to run any visible checks.
func maybeInstallGatewayHook(out *output.Writer, agentName, baseDir string) {
home, err := os.UserHomeDir()
if err != nil {
return
}

// Write the hook script.
hookPath := filepath.Join(home, baseDir, "hooks", "UserPromptSubmit", "onecli_gateway_detect.sh")
existing, err := os.ReadFile(hookPath)
if err != nil || !bytes.Equal(existing, []byte(gatewayDetectHook)) {
if err := os.MkdirAll(filepath.Dir(hookPath), 0o750); err != nil {
return
}
if err := os.WriteFile(hookPath, []byte(gatewayDetectHook), 0o755); err != nil {
return
}
}

// Register in settings.json if not already present.
settingsPath := filepath.Join(home, baseDir, "settings.json")
settings := make(map[string]any)
data, readErr := os.ReadFile(settingsPath)
if readErr == nil && len(data) > 0 {
if err := json.Unmarshal(data, &settings); err != nil {
return
}
}

hookCommand := "bash " + hookPath
hooks, _ := settings["hooks"].(map[string]any)
if hooks == nil {
hooks = make(map[string]any)
}

entries, _ := hooks["UserPromptSubmit"].([]any)

// Check if our hook is already registered.
for _, entry := range entries {
e, _ := entry.(map[string]any)
innerHooks, _ := e["hooks"].([]any)
for _, h := range innerHooks {
hm, _ := h.(map[string]any)
if cmd, _ := hm["command"].(string); cmd == hookCommand {
return
}
}
}

// Add our hook entry.
entries = append(entries, map[string]any{
"matcher": "",
"hooks": []any{
map[string]any{
"type": "command",
"command": hookCommand,
},
},
})
hooks["UserPromptSubmit"] = entries
settings["hooks"] = hooks

if err := os.MkdirAll(filepath.Dir(settingsPath), 0o750); err != nil {
return
}
out2, err := json.MarshalIndent(settings, "", " ")
if err != nil {
return
}
if err := os.WriteFile(settingsPath, append(out2, '\n'), 0o600); err != nil {
return
}
out.Stderr(fmt.Sprintf("onecli: installed gateway hook for %s.", agentName))
}

// injectElectronProxySettings writes http.proxy and http.proxyAuthorization
// into a VS Code-style settings.json so Electron-based editors authenticate
// with the gateway proxy without Chromium's native auth dialog. Returns the
Expand Down
11 changes: 5 additions & 6 deletions cmd/onecli/skill_gateway_fallback.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@
name: onecli-gateway
description: >-
OneCLI Gateway: transparent HTTPS proxy that injects stored credentials
into outbound calls. You MUST use this skill when the user asks you to
read emails, check calendar, access GitHub repos, create issues, check
Stripe payments, or interact with ANY external service or API. Do NOT
use browser extensions or OAuth CLI tools. Make HTTP requests directly;
the gateway injects credentials automatically.
compatibility: Requires HTTPS_PROXY set in environment (automatic when launched via `onecli run`)
into outbound calls. Only use this skill when prompted by the gateway
detection hook or when the user explicitly invokes /onecli-gateway.
Do NOT auto-load this skill based on user intent alone — the hook
handles detection.
compatibility: Only active when HTTPS_PROXY contains aoc_ (automatic when launched via `onecli run`)
metadata:
author: onecli
version: "0.5.0"
Expand Down
Loading