Skip to content
Draft
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
51 changes: 28 additions & 23 deletions install/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -639,7 +639,7 @@ STRIPPED=$(echo "$STRIPPED" | sed -E 's/^[[:space:]]*[A-Za-z_][A-Za-z0-9_]*=[^[:
FIRST=$(echo "$STRIPPED" | awk '{print $1}')

case "$FIRST" in
grep|rg|egrep|fgrep|cat|head|tail|sed|awk|find) ;;
grep|rg|egrep|fgrep|cat|head|tail|find) ;;
*) exit 0 ;;
esac

Expand Down Expand Up @@ -677,7 +677,6 @@ case "$FIRST" in
grep|rg|egrep|fgrep) echo "BLOCKED in indexed repo ($REPO_ROOT): use codedb_search \"<text>\" (codedb_word for an exact identifier, codedb_callers for call sites) instead of $FIRST — ranked + fewer tokens. Native $FIRST is allowed outside this repo or with CODEDB_NO_HOOKS=1." >&2; exit 2 ;;
cat) echo "BLOCKED in indexed repo ($REPO_ROOT): use codedb_read path=<file> (codedb_outline first for a map) instead of cat. CODEDB_NO_HOOKS=1 to disable." >&2; exit 2 ;;
head|tail) echo "BLOCKED in indexed repo ($REPO_ROOT): use codedb_read path=<file> line_start=.. line_end=.. instead of $FIRST. CODEDB_NO_HOOKS=1 to disable." >&2; exit 2 ;;
sed|awk) echo "BLOCKED in indexed repo ($REPO_ROOT): use codedb_edit (op=str_replace) instead of $FIRST for edits. CODEDB_NO_HOOKS=1 to disable." >&2; exit 2 ;;
find) echo "BLOCKED in indexed repo ($REPO_ROOT): use codedb_find (fuzzy names) or codedb_glob (patterns) instead of find. CODEDB_NO_HOOKS=1 to disable." >&2; exit 2 ;;
esac
exit 0
Expand Down Expand Up @@ -808,22 +807,24 @@ main() {
local dest="$INSTALL_DIR/codedb${ext}"

printf " ${D}│${N} %-12s " "codedb"
local tmp="/tmp/codedb.tmp.$$"
local tmp
tmp="$(mktemp "$INSTALL_DIR/.codedb-download.XXXXXX")"
trap 'rm -f "$tmp"' EXIT
if curl -fsSL -A 'codedb-installer' "$url" -o "$tmp" 2>/dev/null; then
# Verify checksum when the release publishes a checksum manifest.
local checksum_text expected_hash checksum_notice="" actual_hash=""
# Keep any existing binary until the downloaded bytes are verified.
local checksum_text expected_hash actual_hash=""
checksum_text="$(curl -fsSL -A 'codedb-installer' "$checksum_url" 2>/dev/null || true)"
expected_hash="$(printf '%s\n' "$checksum_text" | awk "/codedb-${platform}${ext}\$/ { print \$1 }")"
if [ -n "$expected_hash" ]; then
expected_hash="$(printf '%s\n' "$checksum_text" | awk -v asset="codedb-${platform}${ext}" '$2 == asset { count++; if (NF != 2) malformed=1; hash=$1 } END { if (count == 1 && !malformed) print hash }')"
if [[ "$expected_hash" =~ ^[0-9a-f]{64}$ ]]; then
if command -v sha256sum >/dev/null 2>&1; then
actual_hash="$(sha256sum "$tmp" | awk '{print $1}')"
elif command -v shasum >/dev/null 2>&1; then
actual_hash="$(shasum -a 256 "$tmp" | awk '{print $1}')"
fi
if [ -z "$actual_hash" ]; then
# No hashing tool on PATH. Never silently install an unverified
# binary — say so in the same place the skipped-manifest case does.
checksum_notice=" ${Y}warning:${N} checksum NOT verified — neither sha256sum nor shasum is on PATH\n"
printf "${R}failed${N}\n"
printf "\n ${R}error: cannot verify download — install sha256sum or shasum first. Existing binary unchanged.${N}\n" >&2
exit 1
elif [ "$actual_hash" != "$expected_hash" ]; then
rm -f "$tmp"
printf "${R}failed${N}\n"
Expand All @@ -833,10 +834,13 @@ main() {
exit 1
fi
else
checksum_notice=" ${Y}warning:${N} checksum verification skipped (checksums.sha256 unavailable)\n"
printf "${R}failed${N}\n"
printf "\n ${R}error: no unique valid checksum for this platform. Existing binary unchanged; retry when release checksums are available.${N}\n" >&2
exit 1
fi
xattr -c "$tmp" 2>/dev/null || true
mv -f "$tmp" "$dest"
trap - EXIT
chmod +x "$dest"
printf "${G}✓${N}\n"
else
Expand All @@ -848,24 +852,25 @@ main() {

echo ""
printf " ${G}installed${N} ${D}→ $dest${N}\n"
if [ -n "$checksum_notice" ]; then
printf "$checksum_notice"
fi
printf " ${D}claude hook opt-out: CODEDB_NO_HOOKS=1 skips this run; CODEDB_PERSIST_NO_HOOKS=1 or touch ~/.codedb/no-hooks makes it permanent (rm ~/.codedb/no-hooks re-enables)${N}\n"

# Register MCP server in coding tools
echo ""
printf " ${W}registering integrations${N}\n"
echo ""
register_claude "$dest"
register_codex "$dest"
register_codex_policy "$dest"
register_gemini "$dest"
register_cursor "$dest"
register_windsurf_devin "$dest"
register_detected_clients "$dest"
register_deepwiki
register_hooks
if [ -z "${CODEDB_NO_INTEGRATIONS:-}" ]; then
register_claude "$dest"
register_codex "$dest"
register_codex_policy "$dest"
register_gemini "$dest"
register_cursor "$dest"
register_windsurf_devin "$dest"
register_detected_clients "$dest"
register_deepwiki
register_hooks
else
printf " ${D}skipped (CODEDB_NO_INTEGRATIONS)${N}\n"
fi
print_hook_notes "$dest"

# Check PATH
Expand Down
148 changes: 148 additions & 0 deletions scripts/test_installer_safety.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
#!/usr/bin/env python3
"""Installer failure paths must preserve the binary and avoid client config writes."""
import hashlib
import os
import json
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile
import tomllib


def main():
installer = Path(__file__).resolve().parents[1] / 'install/install.sh'
payload = b'#!/bin/sh\nexit 0\n'
digest = hashlib.sha256(payload).hexdigest()
with tempfile.TemporaryDirectory(prefix='codedb-installer-') as directory:
root = Path(directory)
commands = root / 'commands'
commands.mkdir()
for command in ('mkdir', 'mktemp', 'awk', 'rm', 'mv', 'chmod'):
(commands / command).symlink_to(shutil.which(command))
(commands / 'uname').write_text('#!/bin/sh\ncase "$1" in -s) echo Darwin;; -m) echo arm64;; esac\n')
(commands / 'uname').chmod(0o755)
curl = commands / 'curl'
curl.write_text('#!' + sys.executable + '\n' + '''
import os,sys
from pathlib import Path
args=sys.argv[1:];scenario=os.environ['INSTALLER_TEST_SCENARIO']
digest=os.environ['INSTALLER_TEST_DIGEST']
if any(a.endswith('checksums.sha256') for a in args):
if scenario=='unavailable':sys.exit(22)
if scenario=='missing':print(digest+' other-platform');sys.exit(0)
if scenario=='malformed':print('not-a-hash codedb-darwin-arm64');sys.exit(0)
if scenario=='trailing-field':print(digest+' codedb-darwin-arm64 extra-field');sys.exit(0)
if scenario=='duplicate':print((digest+' codedb-darwin-arm64\\n')*2);sys.exit(0)
print(('0'*64 if scenario=='mismatch' else digest)+' codedb-darwin-arm64')
else:
if scenario=='download-failed':sys.exit(22)
Path(args[args.index('-o')+1]).write_bytes(b'#!/bin/sh\\nexit 0\\n')
''')
curl.chmod(0o755)
for scenario in ('unavailable', 'missing', 'malformed', 'trailing-field', 'duplicate', 'mismatch', 'no-hash-tool', 'download-failed', 'valid'):
target = root / (scenario + ' install with spaces')
target.mkdir()
binary = target / 'codedb'
binary.write_bytes(b'existing installation\n')
hash_tool = commands / 'shasum'
if hash_tool.exists():
hash_tool.unlink()
if scenario != 'no-hash-tool':
hash_tool.write_text('#!' + sys.executable + '\nimport hashlib,sys\nfrom pathlib import Path\nprint(hashlib.sha256(Path(sys.argv[-1]).read_bytes()).hexdigest()+" binary")\n')
hash_tool.chmod(0o755)
fixture_home = root / (scenario + ' home')
fixture_home.mkdir()
claude_config = fixture_home / '.claude.json'
existing_config = json.dumps({'mcpServers': {'other': {'command': 'other-tool'}}})
if scenario in ('mismatch', 'trailing-field'):
claude_config.write_text(existing_config)
env = {k: v for k, v in os.environ.items() if not k.startswith('CODEDB_')}
env.update(HOME=str(fixture_home), PATH=str(commands), CODEDB_DIR=str(target),
CODEDB_VERSION='0.2.5855', CODEDB_NO_INTEGRATIONS='1',
INSTALLER_TEST_SCENARIO=scenario, INSTALLER_TEST_DIGEST=digest)
if scenario in ('mismatch', 'trailing-field'):
# A checksum failure must stop before normal client registration.
env.pop('CODEDB_NO_INTEGRATIONS')
env['CODEDB_INSTALL_DEEPWIKI'] = '0'
env['PATH'] += os.pathsep + os.environ['PATH']
done = subprocess.run(['/bin/bash', str(installer)], env=env, capture_output=True, text=True, timeout=20)
success = scenario == 'valid'
assert (done.returncode == 0) == success, (scenario, done.stdout, done.stderr)
assert binary.read_bytes() == (payload if success else b'existing installation\n'), scenario
assert not list(target.glob('.codedb-download.*')), scenario
assert 'unbound variable' not in done.stderr, done.stderr
if scenario in ('mismatch', 'trailing-field'):
assert claude_config.read_text() == existing_config, scenario
else:
assert not claude_config.exists(), scenario
assert not (fixture_home / '.codex').exists(), scenario
assert not (fixture_home / '.claude').exists(), scenario
print('PASS:', scenario)

# Drive the normal main() path inside a disposable HOME. This must
# reach detected-client registration and install the hook we exercise.
fixture_home = root / 'fixture-user'
fixture_home.mkdir()
project = root / 'project'
project.mkdir()
project = project.resolve()
registration = fixture_home / '.codedb/projects/test/project.txt'
registration.parent.mkdir(parents=True)
registration.write_text(str(project) + '\n')
claude_config = fixture_home / '.claude.json'
claude_config.write_text(json.dumps({'mcpServers': {'other': {'command': 'other-tool'}}}))
codex_config = fixture_home / '.codex/config.toml'
codex_config.parent.mkdir()
codex_config.write_text('[mcp_servers.other]\ncommand = "other-tool"\n')
qwen_config = fixture_home / '.qwen/settings.json'
qwen_config.parent.mkdir()
qwen_config.write_text(json.dumps({'mcpServers': {'other': {'command': 'other-tool'}}}))
normal_target = root / 'normal install with spaces'
normal_target.mkdir()
binary = normal_target / 'codedb'
binary.write_bytes(b'existing installation\n')
fake_codedb = commands / 'codedb'
fake_codedb.write_bytes(payload)
fake_codedb.chmod(0o755)
normal_env = {k: v for k, v in os.environ.items() if not k.startswith('CODEDB_')}
normal_env.update(HOME=str(fixture_home), PATH=str(commands) + os.pathsep + os.environ['PATH'],
CODEDB_DIR=str(normal_target), CODEDB_VERSION='0.2.5855',
CODEDB_INSTALL_DEEPWIKI='0', INSTALLER_TEST_SCENARIO='valid',
INSTALLER_TEST_DIGEST=digest)
done = subprocess.run(['/bin/bash', str(installer)], env=normal_env,
capture_output=True, text=True, timeout=20)
assert done.returncode == 0, (done.stdout, done.stderr)
assert binary.read_bytes() == payload
assert not list(normal_target.glob('.codedb-download.*'))
print('PASS: normal install through main')

configs = [
('claude', json.loads(claude_config.read_text())['mcpServers']),
('codex', tomllib.loads(codex_config.read_text())['mcp_servers']),
('qwen detected client', json.loads(qwen_config.read_text())['mcpServers']),
]
for name, config in configs:
assert config['other']['command'] == 'other-tool'
assert config['codedb']['command'] == str(binary)
assert config['codedb']['args'] == ['mcp']
print('PASS: normal', name, 'registration preserves other servers')

hook = fixture_home / '.claude/hooks/codedb-block-legacy.sh'
assert hook.is_file() and os.access(hook, os.X_OK)
settings = json.loads((fixture_home / '.claude/settings.json').read_text())
assert any('codedb-block-legacy.sh' in entry['hooks'][0]['command']
for entry in settings['hooks']['PreToolUse'])
for command, expected in [('sed -i s/old/new/ code.py', 0), ('awk "{print}" code.py', 0),
('rg handler .', 2), ('python3 edit.py', 0)]:
done = subprocess.run(['/bin/bash', str(hook)], cwd=project, env=normal_env,
input=json.dumps({'tool_input': {'command': command}}),
capture_output=True, text=True, timeout=10)
assert done.returncode == expected, (command, done.stderr)
assert 'codedb_edit' not in done.stderr
print('PASS: installed hook', command.split()[0])


if __name__ == '__main__':
main()
51 changes: 28 additions & 23 deletions website/app/install_script.sh
Original file line number Diff line number Diff line change
Expand Up @@ -639,7 +639,7 @@ STRIPPED=$(echo "$STRIPPED" | sed -E 's/^[[:space:]]*[A-Za-z_][A-Za-z0-9_]*=[^[:
FIRST=$(echo "$STRIPPED" | awk '{print $1}')

case "$FIRST" in
grep|rg|egrep|fgrep|cat|head|tail|sed|awk|find) ;;
grep|rg|egrep|fgrep|cat|head|tail|find) ;;
*) exit 0 ;;
esac

Expand Down Expand Up @@ -677,7 +677,6 @@ case "$FIRST" in
grep|rg|egrep|fgrep) echo "BLOCKED in indexed repo ($REPO_ROOT): use codedb_search \"<text>\" (codedb_word for an exact identifier, codedb_callers for call sites) instead of $FIRST — ranked + fewer tokens. Native $FIRST is allowed outside this repo or with CODEDB_NO_HOOKS=1." >&2; exit 2 ;;
cat) echo "BLOCKED in indexed repo ($REPO_ROOT): use codedb_read path=<file> (codedb_outline first for a map) instead of cat. CODEDB_NO_HOOKS=1 to disable." >&2; exit 2 ;;
head|tail) echo "BLOCKED in indexed repo ($REPO_ROOT): use codedb_read path=<file> line_start=.. line_end=.. instead of $FIRST. CODEDB_NO_HOOKS=1 to disable." >&2; exit 2 ;;
sed|awk) echo "BLOCKED in indexed repo ($REPO_ROOT): use codedb_edit (op=str_replace) instead of $FIRST for edits. CODEDB_NO_HOOKS=1 to disable." >&2; exit 2 ;;
find) echo "BLOCKED in indexed repo ($REPO_ROOT): use codedb_find (fuzzy names) or codedb_glob (patterns) instead of find. CODEDB_NO_HOOKS=1 to disable." >&2; exit 2 ;;
esac
exit 0
Expand Down Expand Up @@ -808,22 +807,24 @@ main() {
local dest="$INSTALL_DIR/codedb${ext}"

printf " ${D}│${N} %-12s " "codedb"
local tmp="/tmp/codedb.tmp.$$"
local tmp
tmp="$(mktemp "$INSTALL_DIR/.codedb-download.XXXXXX")"
trap 'rm -f "$tmp"' EXIT
if curl -fsSL -A 'codedb-installer' "$url" -o "$tmp" 2>/dev/null; then
# Verify checksum when the release publishes a checksum manifest.
local checksum_text expected_hash checksum_notice="" actual_hash=""
# Keep any existing binary until the downloaded bytes are verified.
local checksum_text expected_hash actual_hash=""
checksum_text="$(curl -fsSL -A 'codedb-installer' "$checksum_url" 2>/dev/null || true)"
expected_hash="$(printf '%s\n' "$checksum_text" | awk "/codedb-${platform}${ext}\$/ { print \$1 }")"
if [ -n "$expected_hash" ]; then
expected_hash="$(printf '%s\n' "$checksum_text" | awk -v asset="codedb-${platform}${ext}" '$2 == asset { count++; if (NF != 2) malformed=1; hash=$1 } END { if (count == 1 && !malformed) print hash }')"
if [[ "$expected_hash" =~ ^[0-9a-f]{64}$ ]]; then
if command -v sha256sum >/dev/null 2>&1; then
actual_hash="$(sha256sum "$tmp" | awk '{print $1}')"
elif command -v shasum >/dev/null 2>&1; then
actual_hash="$(shasum -a 256 "$tmp" | awk '{print $1}')"
fi
if [ -z "$actual_hash" ]; then
# No hashing tool on PATH. Never silently install an unverified
# binary — say so in the same place the skipped-manifest case does.
checksum_notice=" ${Y}warning:${N} checksum NOT verified — neither sha256sum nor shasum is on PATH\n"
printf "${R}failed${N}\n"
printf "\n ${R}error: cannot verify download — install sha256sum or shasum first. Existing binary unchanged.${N}\n" >&2
exit 1
elif [ "$actual_hash" != "$expected_hash" ]; then
rm -f "$tmp"
printf "${R}failed${N}\n"
Expand All @@ -833,10 +834,13 @@ main() {
exit 1
fi
else
checksum_notice=" ${Y}warning:${N} checksum verification skipped (checksums.sha256 unavailable)\n"
printf "${R}failed${N}\n"
printf "\n ${R}error: no unique valid checksum for this platform. Existing binary unchanged; retry when release checksums are available.${N}\n" >&2
exit 1
fi
xattr -c "$tmp" 2>/dev/null || true
mv -f "$tmp" "$dest"
trap - EXIT
chmod +x "$dest"
printf "${G}✓${N}\n"
else
Expand All @@ -848,24 +852,25 @@ main() {

echo ""
printf " ${G}installed${N} ${D}→ $dest${N}\n"
if [ -n "$checksum_notice" ]; then
printf "$checksum_notice"
fi
printf " ${D}claude hook opt-out: CODEDB_NO_HOOKS=1 skips this run; CODEDB_PERSIST_NO_HOOKS=1 or touch ~/.codedb/no-hooks makes it permanent (rm ~/.codedb/no-hooks re-enables)${N}\n"

# Register MCP server in coding tools
echo ""
printf " ${W}registering integrations${N}\n"
echo ""
register_claude "$dest"
register_codex "$dest"
register_codex_policy "$dest"
register_gemini "$dest"
register_cursor "$dest"
register_windsurf_devin "$dest"
register_detected_clients "$dest"
register_deepwiki
register_hooks
if [ -z "${CODEDB_NO_INTEGRATIONS:-}" ]; then
register_claude "$dest"
register_codex "$dest"
register_codex_policy "$dest"
register_gemini "$dest"
register_cursor "$dest"
register_windsurf_devin "$dest"
register_detected_clients "$dest"
register_deepwiki
register_hooks
else
printf " ${D}skipped (CODEDB_NO_INTEGRATIONS)${N}\n"
fi
print_hook_notes "$dest"

# Check PATH
Expand Down
Loading
Loading