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
1 change: 1 addition & 0 deletions .github/workflows/claude-code-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ jobs:
plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
plugins: 'code-review@claude-code-plugins'
prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}'
allowed_tools: 'Bash(gh pr *),Bash(gh api *),Bash(git diff *),Bash(git log *),Read,Glob,Grep'
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
# or https://code.claude.com/docs/en/cli-reference for available options

3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ JS source is plain JavaScript (ES modules) in `src/`. No transpilation step. The
| `cycles.js` | Circular dependency detection |
| `export.js` | DOT/Mermaid/JSON graph export |
| `watcher.js` | Watch mode for incremental rebuilds |
| `config.js` | `.codegraphrc.json` loading |
| `config.js` | `.codegraphrc.json` loading, env overrides, `apiKeyCommand` secret resolution |
| `constants.js` | `EXTENSIONS` (derived from parser registry) and `IGNORE_DIRS` constants |
| `native.js` | Native napi-rs addon loader with WASM fallback |
| `resolve.js` | Import resolution (supports native batch mode) |
Expand All @@ -64,6 +64,7 @@ JS source is plain JavaScript (ES modules) in `src/`. No transpilation step. The
- Non-required parsers (all except JS/TS/TSX) fail gracefully if their WASM grammar is unavailable
- Import resolution uses a 6-level priority system with confidence scoring (import-aware → same-file → directory → parent → global → method hierarchy)
- Incremental builds track file hashes in the DB to skip unchanged files
- **Credential resolution:** `loadConfig` pipeline is `mergeConfig → applyEnvOverrides → resolveSecrets`. The `apiKeyCommand` config field shells out to an external secret manager via `execFileSync` (no shell). Priority: command output > env var > file config > defaults. On failure, warns and falls back gracefully

**Database:** SQLite at `.codegraph/graph.db` with tables: `nodes`, `edges`, `metadata`, `embeddings`

Expand Down
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,7 @@ See **[docs/recommended-practices.md](docs/recommended-practices.md)** for integ
- **CI/CD** — PR impact comments, threshold gates, graph caching
- **AI agents** — MCP server, CLAUDE.md templates, Claude Code hooks
- **Developer workflow** — watch mode, explore-before-you-edit, semantic search
- **Secure credentials** — `apiKeyCommand` with 1Password, Bitwarden, Vault, macOS Keychain, `pass`

## 🔁 CI / GitHub Actions

Expand Down Expand Up @@ -395,6 +396,23 @@ Create a `.codegraphrc.json` in your project root to customize behavior:
}
```

### LLM credentials

Codegraph supports an `apiKeyCommand` field for secure credential management. Instead of storing API keys in config files or environment variables, you can shell out to a secret manager at runtime:

```json
{
"llm": {
"provider": "openai",
"apiKeyCommand": "op read op://vault/openai/api-key"
}
}
```

The command is split on whitespace and executed with `execFileSync` (no shell injection risk). Priority: **command output > `CODEGRAPH_LLM_API_KEY` env var > file config**. On failure, codegraph warns and falls back to the next source.

Works with any secret manager: 1Password CLI (`op`), Bitwarden (`bw`), `pass`, HashiCorp Vault, macOS Keychain (`security`), AWS Secrets Manager, etc.

## 📖 Programmatic API

Codegraph also exports a full API for use in your own tools:
Expand Down
85 changes: 85 additions & 0 deletions docs/recommended-practices.md
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,91 @@ codegraph search "catch exception; format error response; report failure to clie

---

## Secure Credential Management

Codegraph's LLM features (semantic search with LLM-generated descriptions, future `codegraph ask`) require an API key. Use `apiKeyCommand` to fetch it from a secret manager at runtime instead of hardcoding it in config files or leaking it through environment variables.

### Why not environment variables?

Environment variables are better than plaintext in config files, but they still leak via `ps e`, `/proc/<pid>/environ`, child processes, shell history, and CI logs. `apiKeyCommand` keeps the secret in your vault and only materializes it in process memory for the duration of the call.

### Examples

**1Password CLI:**

```json
{
"llm": {
"provider": "openai",
"apiKeyCommand": "op read op://Development/openai/api-key"
}
}
```

**Bitwarden CLI:**

```json
{
"llm": {
"provider": "anthropic",
"apiKeyCommand": "bw get password anthropic-api-key"
}
}
```

**macOS Keychain:**

```json
{
"llm": {
"provider": "openai",
"apiKeyCommand": "security find-generic-password -s codegraph-llm -w"
}
}
```

**HashiCorp Vault:**

```json
{
"llm": {
"provider": "openai",
"apiKeyCommand": "vault kv get -field=api_key secret/codegraph/openai"
}
}
```

**`pass` (GPG-encrypted):**

```json
{
"llm": {
"provider": "openai",
"apiKeyCommand": "pass show codegraph/openai-key"
}
}
```

### Priority chain

The resolution order is:

1. **`apiKeyCommand`** output (highest priority)
2. **`CODEGRAPH_LLM_API_KEY`** environment variable
3. **`llm.apiKey`** in config file
4. **`null`** (default)

If the command fails (timeout, not found, non-zero exit), codegraph logs a warning and falls back to the next available source. The command has a 10-second timeout.

### Security notes

- The command is split on whitespace and executed with `execFileSync` (array args, no shell) — no shell injection risk
- stdout is captured; stderr is discarded
- The resolved key is held only in process memory, never written to disk
- Keep `.codegraphrc.json` out of version control if it contains `apiKeyCommand` paths specific to your vault layout, or use a shared command that works across the team

---

## .gitignore

Add the codegraph database to `.gitignore` — it's a build artifact:
Expand Down
48 changes: 45 additions & 3 deletions src/config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import { debug } from './logger.js';
import { debug, warn } from './logger.js';

export const CONFIG_FILES = ['.codegraphrc.json', '.codegraphrc', 'codegraph.config.json'];

Expand All @@ -18,6 +19,10 @@ export const DEFAULTS = {
defaultDepth: 3,
defaultLimit: 20,
},
embeddings: { model: 'minilm', llmProvider: null },
llm: { provider: null, model: null, baseUrl: null, apiKey: null, apiKeyCommand: null },
search: { defaultMinScore: 0.2, rrfK: 60, topK: 15 },
ci: { failOnCycles: false, impactThreshold: null },
};

/**
Expand All @@ -33,13 +38,50 @@ export function loadConfig(cwd) {
const raw = fs.readFileSync(filePath, 'utf-8');
const config = JSON.parse(raw);
debug(`Loaded config from ${filePath}`);
return mergeConfig(DEFAULTS, config);
return resolveSecrets(applyEnvOverrides(mergeConfig(DEFAULTS, config)));
} catch (err) {
debug(`Failed to parse config ${filePath}: ${err.message}`);
}
}
}
return { ...DEFAULTS };
return resolveSecrets(applyEnvOverrides({ ...DEFAULTS }));
}

const ENV_LLM_MAP = {
CODEGRAPH_LLM_PROVIDER: 'provider',
CODEGRAPH_LLM_API_KEY: 'apiKey',
CODEGRAPH_LLM_MODEL: 'model',
};

export function applyEnvOverrides(config) {
for (const [envKey, field] of Object.entries(ENV_LLM_MAP)) {
if (process.env[envKey] !== undefined) {
config.llm[field] = process.env[envKey];
}
}
return config;
}

export function resolveSecrets(config) {
const cmd = config.llm.apiKeyCommand;
if (typeof cmd !== 'string' || cmd.trim() === '') return config;

const parts = cmd.trim().split(/\s+/);
const [executable, ...args] = parts;
try {
const result = execFileSync(executable, args, {
encoding: 'utf-8',
timeout: 10_000,
maxBuffer: 64 * 1024,
stdio: ['ignore', 'pipe', 'pipe'],
}).trim();
if (result) {
config.llm.apiKey = result;
}
} catch (err) {
warn(`apiKeyCommand failed: ${err.message}`);
}
return config;
}

function mergeConfig(defaults, overrides) {
Expand Down
Loading
Loading