From 8625918d307c3a0e4277016501d18b17f711c82d Mon Sep 17 00:00:00 2001 From: AIOSAI Date: Sat, 16 May 2026 10:05:04 -0700 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20private=20integrations=20architectu?= =?UTF-8?q?re=20=E2=80=94=20generic=20hook=20auto-discovers=20apps/integra?= =?UTF-8?q?tions/*/private=5Fprompt.md=20for=20per-branch=20private=20proj?= =?UTF-8?q?ect=20injection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/hooks/branch_prompt_loader.py | 5 +++++ src/aipass/devpulse/apps/integrations/README.md | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 src/aipass/devpulse/apps/integrations/README.md diff --git a/.claude/hooks/branch_prompt_loader.py b/.claude/hooks/branch_prompt_loader.py index c3d2e94e..56f5ff69 100644 --- a/.claude/hooks/branch_prompt_loader.py +++ b/.claude/hooks/branch_prompt_loader.py @@ -74,6 +74,11 @@ def main(): branch_name = branch_root.name.upper() print(f"\n# Branch Context: {branch_name}\n\n{content}") + integrations_dir = branch_root / "apps" / "integrations" + if integrations_dir.is_dir(): + for prompt in sorted(integrations_dir.glob("*/private_prompt.md")): + print(f"\n{prompt.read_text().strip()}") + if __name__ == "__main__": import sys diff --git a/src/aipass/devpulse/apps/integrations/README.md b/src/aipass/devpulse/apps/integrations/README.md new file mode 100644 index 00000000..e3401f5f --- /dev/null +++ b/src/aipass/devpulse/apps/integrations/README.md @@ -0,0 +1,5 @@ +# Integrations + +Private project integrations. Contents gitignored — only this README is tracked. + +Drop project-specific scripts here. They're available to the branch agent but invisible to git. From d7dbb6291b1544954f6ba10e4487f04511e59d86 Mon Sep 17 00:00:00 2001 From: AIOSAI Date: Sat, 16 May 2026 10:38:20 -0700 Subject: [PATCH 2/7] =?UTF-8?q?fix(security):=20block=20Python=20subproces?= =?UTF-8?q?s=20git=20bypass=20in=20git=5Fgate.py=20=E2=80=94=20detects=20s?= =?UTF-8?q?ubprocess.run/call/Popen/os.system=20wrapping=20git/gh=20comman?= =?UTF-8?q?ds?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/hooks/git_gate.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.claude/hooks/git_gate.py b/.claude/hooks/git_gate.py index 77d5df46..afdb2bb8 100755 --- a/.claude/hooks/git_gate.py +++ b/.claude/hooks/git_gate.py @@ -146,10 +146,17 @@ def main(): cmd = tool_input.get("command", "") if not cmd: return + + # Subprocess bypass detection — scan raw command for git/gh inside + # subprocess.run/call/Popen/os.system patterns before stripping quotes. + if re.search(r"subprocess\.\w+|os\.system|os\.popen|Popen", cmd): + if re.search(r"['\"]git['\"]|['\"]gh['\"]", cmd): + _block(GIT_REDIRECT) + # Strip quoted strings before matching — text inside "..." or '...' is data # (PR descriptions, commit messages, examples in docs), not code to enforce. scan = re.sub(r'"(?:[^"\\]|\\.)*"', '""', cmd) - scan = re.sub(r"'(?:[^'\\]|\\.)*'", "''", scan) + scan = re.sub(r"'(?:[^'\\]|\\.)*'", "''", cmd) if ( BLOCKED_GIT_RE.search(scan) or BLOCKED_GIT_STASH_RE.search(scan) From a22a1b174b7e634a8d3a34093ba7ca6dbe04717f Mon Sep 17 00:00:00 2001 From: AIOSAI Date: Sat, 16 May 2026 11:48:37 -0700 Subject: [PATCH 3/7] =?UTF-8?q?fix(security):=20broaden=20git=5Fgate=20sub?= =?UTF-8?q?process=20bypass=20detection=20=E2=80=94=20word-boundary=20matc?= =?UTF-8?q?hing=20catches=20os.system=20string-style,=20bare=20popen,=20an?= =?UTF-8?q?d=20escaped-quote=20patterns=20(Issue=20#561)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/hooks/git_gate.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.claude/hooks/git_gate.py b/.claude/hooks/git_gate.py index afdb2bb8..d367291f 100755 --- a/.claude/hooks/git_gate.py +++ b/.claude/hooks/git_gate.py @@ -148,9 +148,9 @@ def main(): return # Subprocess bypass detection — scan raw command for git/gh inside - # subprocess.run/call/Popen/os.system patterns before stripping quotes. - if re.search(r"subprocess\.\w+|os\.system|os\.popen|Popen", cmd): - if re.search(r"['\"]git['\"]|['\"]gh['\"]", cmd): + # subprocess/os execution patterns before stripping quotes. + if re.search(r"subprocess\.\w+|os\.system|os\.popen|Popen|(? Date: Sat, 16 May 2026 11:58:22 -0700 Subject: [PATCH 4/7] =?UTF-8?q?fix(security):=20block=20full=20binary=20pa?= =?UTF-8?q?th=20bypass=20(/usr/bin/git,=20/usr/local/bin/gh)=20in=20git=5F?= =?UTF-8?q?gate=20=E2=80=94=20Issue=20#561?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/hooks/git_gate.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/.claude/hooks/git_gate.py b/.claude/hooks/git_gate.py index d367291f..aa681145 100755 --- a/.claude/hooks/git_gate.py +++ b/.claude/hooks/git_gate.py @@ -43,6 +43,12 @@ BLOCKED_GIT_RE = re.compile( r"(" + "|".join(BLOCKED_GIT_VERBS) + r")\b" ) +# Full binary path bypass: /usr/bin/git, /usr/local/bin/git, ./git, etc. +BLOCKED_GIT_PATH_RE = re.compile( + r"/git\s+(?:--?[A-Za-z][A-Za-z0-9_-]*(?:[= ][^\s]+)?\s+)*" + r"(" + "|".join(BLOCKED_GIT_VERBS) + r")\b" +) + BLOCKED_GIT_STASH_RE = re.compile(r"(? Date: Sat, 16 May 2026 12:00:59 -0700 Subject: [PATCH 5/7] =?UTF-8?q?fix(security):=20detect=20git=20commands=20?= =?UTF-8?q?inside=20script=20files=20=E2=80=94=20reads=20file=20contents?= =?UTF-8?q?=20when=20bash/sh/source=20invoked=20(Issue=20#561)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/hooks/git_gate.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/.claude/hooks/git_gate.py b/.claude/hooks/git_gate.py index aa681145..66ebc2fe 100755 --- a/.claude/hooks/git_gate.py +++ b/.claude/hooks/git_gate.py @@ -165,6 +165,22 @@ def main(): if re.search(r"\bgit\b|\bgh\b", cmd): _block(GIT_REDIRECT) + # Script-file bypass detection — read file contents when bash/sh runs a script. + script_match = re.search(r"(?:^|[;&|]\s*)(?:bash|sh|source|\.)\s+([^\s;&|]+)", cmd) + if script_match: + script_path = script_match.group(1) + if not os.path.isabs(script_path): + script_path = os.path.join(cwd, script_path) + try: + content = Path(script_path).read_text(encoding="utf-8", errors="ignore") + if BLOCKED_GIT_RE.search(content) or BLOCKED_GIT_PATH_RE.search(content): + _block(GIT_REDIRECT) + if BLOCKED_GH_RE.search(content) or BLOCKED_GH_PATH_RE.search(content): + if not (_cwd_branch(cwd) in TRUSTED_HOOK_EDITORS or _is_project_owner(cwd)): + _block(GH_REDIRECT) + except (OSError, UnicodeDecodeError): + pass + # Strip quoted strings before matching — text inside "..." or '...' is data # (PR descriptions, commit messages, examples in docs), not code to enforce. scan = re.sub(r'"(?:[^"\\]|\\.)*"', '""', cmd) From 8e730edb35e1fe1e027af6abd432e6abcd291491 Mon Sep 17 00:00:00 2001 From: AIOSAI Date: Sat, 16 May 2026 12:03:48 -0700 Subject: [PATCH 6/7] =?UTF-8?q?fix(security):=20close=20remaining=20git=5F?= =?UTF-8?q?gate=20bypasses=20=E2=80=94=20pipe-to-shell,=20python=20scripts?= =?UTF-8?q?,=20xargs,=20variable=20expansion=20(Issue=20#561)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .claude/hooks/git_gate.py | 46 ++++++++++++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/.claude/hooks/git_gate.py b/.claude/hooks/git_gate.py index 66ebc2fe..44ed4f5f 100755 --- a/.claude/hooks/git_gate.py +++ b/.claude/hooks/git_gate.py @@ -165,22 +165,52 @@ def main(): if re.search(r"\bgit\b|\bgh\b", cmd): _block(GIT_REDIRECT) - # Script-file bypass detection — read file contents when bash/sh runs a script. - script_match = re.search(r"(?:^|[;&|]\s*)(?:bash|sh|source|\.)\s+([^\s;&|]+)", cmd) + # Script-file bypass detection — read file contents when an interpreter runs a file. + script_match = re.search( + r"(?:^|[;&|]\s*)(?:bash|sh|source|python3?|\.)\s+([^\s;&|]+)", cmd + ) if script_match: script_path = script_match.group(1) - if not os.path.isabs(script_path): - script_path = os.path.join(cwd, script_path) + if not script_path.startswith("-"): + if not os.path.isabs(script_path): + script_path = os.path.join(cwd, script_path) + try: + content = Path(script_path).read_text(encoding="utf-8", errors="ignore") + if BLOCKED_GIT_RE.search(content) or BLOCKED_GIT_PATH_RE.search(content): + _block(GIT_REDIRECT) + if re.search(r"\bgit\b|\bgh\b", content): + if re.search(r"subprocess|os\.system|os\.popen|Popen", content): + _block(GIT_REDIRECT) + if BLOCKED_GH_RE.search(content) or BLOCKED_GH_PATH_RE.search(content): + if not (_cwd_branch(cwd) in TRUSTED_HOOK_EDITORS or _is_project_owner(cwd)): + _block(GH_REDIRECT) + except (OSError, UnicodeDecodeError): + pass + + # Pipe-to-shell and stdin redirect: cat file | bash, bash < file + pipe_match = re.search(r"(?:cat|head|tail)\s+([^\s;&|]+)\s*\|\s*(?:bash|sh)\b", cmd) + if not pipe_match: + pipe_match = re.search(r"(?:bash|sh)\s*<\s*([^\s;&|]+)", cmd) + if pipe_match: + piped_path = pipe_match.group(1) + if not os.path.isabs(piped_path): + piped_path = os.path.join(cwd, piped_path) try: - content = Path(script_path).read_text(encoding="utf-8", errors="ignore") + content = Path(piped_path).read_text(encoding="utf-8", errors="ignore") if BLOCKED_GIT_RE.search(content) or BLOCKED_GIT_PATH_RE.search(content): _block(GIT_REDIRECT) - if BLOCKED_GH_RE.search(content) or BLOCKED_GH_PATH_RE.search(content): - if not (_cwd_branch(cwd) in TRUSTED_HOOK_EDITORS or _is_project_owner(cwd)): - _block(GH_REDIRECT) except (OSError, UnicodeDecodeError): pass + # xargs with git/gh — command construction bypass + if re.search(r"\bxargs\b.*\bgit\b|\bxargs\b.*\bgh\b", cmd): + _block(GIT_REDIRECT) + + # Variable assignment bypass: cmd=git; $cmd commit + if re.search(r"=\s*git\b|=\s*gh\b", cmd): + if re.search(r"\$\w*\s+" + "(" + "|".join(BLOCKED_GIT_VERBS) + ")", cmd): + _block(GIT_REDIRECT) + # Strip quoted strings before matching — text inside "..." or '...' is data # (PR descriptions, commit messages, examples in docs), not code to enforce. scan = re.sub(r'"(?:[^"\\]|\\.)*"', '""', cmd) From ce9d2de985b992ddd3374e3c41d0d94641430799 Mon Sep 17 00:00:00 2001 From: AIOSAI Date: Sat, 16 May 2026 12:11:07 -0700 Subject: [PATCH 7/7] ci: add macOS test workflow (mirrors windows-test.yml) --- .github/workflows/macos-test.yml | 50 ++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 .github/workflows/macos-test.yml diff --git a/.github/workflows/macos-test.yml b/.github/workflows/macos-test.yml new file mode 100644 index 00000000..7db5ca7e --- /dev/null +++ b/.github/workflows/macos-test.yml @@ -0,0 +1,50 @@ +name: macOS Test + +on: + workflow_dispatch: + push: + branches: [main, dev] + paths: + - 'setup.sh' + - 'src/aipass/*/apps/handlers/__init__.py' + - 'src/aipass/drone/cli.py' + - 'pyproject.toml' + pull_request: + paths: + - 'setup.sh' + - 'src/aipass/*/apps/handlers/__init__.py' + - 'src/aipass/drone/cli.py' + - 'pyproject.toml' + +jobs: + macos-setup: + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Run setup.sh + run: bash setup.sh + + - name: Verify drone CLI + run: | + source .venv/bin/activate + drone --version + drone systems + drone @seedgo --help + + - name: Run full test suite + run: | + source .venv/bin/activate + pytest -v --tb=short --rootdir=. 2>&1 | tee pytest-output.txt + echo "EXIT_CODE=${PIPESTATUS[0]}" >> "$GITHUB_ENV" + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: macos-pytest-results + path: pytest-output.txt