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
2 changes: 1 addition & 1 deletion .claude/agents/tapps-docs-reviewer.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description: >-
Review documentation quality using DocsMCP validation tools. Checks drift,
freshness, completeness, links, and Diataxis balance.
tools: Read, Glob, Grep, Write, Edit
model: claude-sonnet-4-6
model: claude-sonnet-5
maxTurns: 20
permissionMode: plan
memory: project
Expand Down
2 changes: 1 addition & 1 deletion .claude/agents/tapps-frontend-reviewer.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description: >-
Review UI/UX and frontend changes using domain playbooks and TAPPS quality
gates. Use for React, CSS, accessibility, or layout work.
tools: Read, Glob, Grep, Write, Edit
model: claude-sonnet-4-6
model: claude-sonnet-5
maxTurns: 20
permissionMode: acceptEdits
memory: project
Expand Down
2 changes: 1 addition & 1 deletion .claude/agents/tapps-researcher.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description: >-
Look up documentation, consult domain experts, and research best practices
for the technologies used in this project.
tools: Read, Glob, Grep
model: claude-sonnet-4-6
model: claude-sonnet-5
maxTurns: 15
permissionMode: plan
memory: project
Expand Down
2 changes: 1 addition & 1 deletion .claude/agents/tapps-review-fixer.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ description: >-
and validates the result passes the quality gate. Use in worktrees for
parallel multi-file review pipelines.
tools: Read, Glob, Grep, Write, Edit, Bash
model: claude-sonnet-4-6
model: claude-sonnet-5
maxTurns: 25
permissionMode: acceptEdits
memory: project
Expand Down
2 changes: 1 addition & 1 deletion .claude/agents/tapps-reviewer.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description: >-
Use proactively to review code quality, run security scans, and enforce
quality gates after editing Python files.
tools: Read, Glob, Grep, Write, Edit
model: claude-sonnet-4-6
model: claude-sonnet-5
maxTurns: 20
permissionMode: acceptEdits
memory: project
Expand Down
2 changes: 1 addition & 1 deletion .claude/hooks/tapps-post-docs-validate.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# tapps-mcp-hook-version: 3.12.78
# tapps-mcp-hook-version: 3.12.83
# tapps-mcp-hook-content-sha: 6934aae7
# TappsMCP PostToolUse hook — Linear gate sentinel writer (TAP-981 / TAP-1328)
# Writes .tapps-mcp/.linear-validate-sentinel ONLY when the validate call
Expand Down
6 changes: 3 additions & 3 deletions .claude/hooks/tapps-post-edit.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# tapps-mcp-hook-version: 3.12.78
# tapps-mcp-hook-content-sha: 993e0bea
# tapps-mcp-hook-version: 3.12.83
# tapps-mcp-hook-content-sha: 12bc53fe
# TappsMCP PostToolUse hook (Edit/Write) — TAP-1326 / TAP-1330
# Detects new external imports requiring tapps_lookup_docs. Advisory only;
# the Stop hook enforces the completion gate.
Expand Down Expand Up @@ -100,7 +100,7 @@ if [ "$SKILL_GUARD" = "1" ]; then
echo "$FILE: this edit landed inside the BEGIN/END managed block — tapps_upgrade regenerates that region and the edit will be lost. Move project-specific content below the END marker instead." >&2
fi
case "$FILE" in
*.py|*.pyi|*.ts|*.tsx|*.js|*.jsx|*.go|*.rs)
*.cjs|*.go|*.js|*.jsx|*.mjs|*.py|*.pyi|*.rs|*.ts|*.tsx)
echo "Edited: $FILE — run tapps_quick_check after this edit." >&2
if [ -n "$LIBS" ]; then
echo "Imports detected ($LIBS) — call tapps_lookup_docs(library=..., topic=...) **before editing** code that uses those APIs (TAP-1330). Retrospective lookups at finish-task do not excuse skipped pre-edit lookups." >&2
Expand Down
2 changes: 1 addition & 1 deletion .claude/hooks/tapps-post-linear-list.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# tapps-mcp-hook-version: 3.12.78
# tapps-mcp-hook-version: 3.12.83
# tapps-mcp-hook-content-sha: c9dc8b49
# TappsMCP PostToolUse hook — Linear list_issues auto-populate (TAP-1412)
# After a successful mcp__plugin_linear_linear__list_issues call, write the
Expand Down
2 changes: 1 addition & 1 deletion .claude/hooks/tapps-post-linear-snapshot-get.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# tapps-mcp-hook-version: 3.12.78
# tapps-mcp-hook-version: 3.12.83
# tapps-mcp-hook-content-sha: 5a2c3acd
# TappsMCP PostToolUse hook — Linear cache-gate sentinel writer (TAP-1224)
# Writes a per-(team, project, state, label, limit) sentinel on BOTH
Expand Down
2 changes: 1 addition & 1 deletion .claude/hooks/tapps-post-report.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# tapps-mcp-hook-version: 3.12.78
# tapps-mcp-hook-version: 3.12.83
# tapps-mcp-hook-content-sha: dd5a53bb
# TappsMCP PostToolUse hook (tapps_report)
# Reads the report sidecar progress file and echoes a summary.
Expand Down
43 changes: 36 additions & 7 deletions .claude/hooks/tapps-post-session-start.sh
Original file line number Diff line number Diff line change
@@ -1,18 +1,47 @@
#!/usr/bin/env bash
# tapps-mcp-hook-version: 3.12.78
# tapps-mcp-hook-content-sha: b03ed009
# tapps-mcp-hook-version: 3.12.83
# tapps-mcp-hook-content-sha: 2aae887b
# TappsMCP PostToolUse hook — session-start sentinel writer.
# Writes .session-start-done-<SID> AFTER tapps_session_start actually returns,
# proving the tool ran (not merely that the SessionStart hook fired). The
# pre-session-start gate reads this sentinel to release TappsMCP quality tools.
# Writes .session-start-done-<SID> ONLY when tapps_session_start actually
# returned a success envelope, proving the *tool* ran (not merely that the
# SessionStart hook fired, and not merely that the *name* was called).
# TAP-7018: a tool_relocated pointer error (a retired-server registration
# calling back "this name moved") used to satisfy this gate on tool name
# alone, silently releasing every downstream quality tool with session_start
# never having actually run.
INPUT=$(cat)
TOOL=$(printf '%s' "$INPUT" | sed -n 's/.*"tool_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)
PYBIN=$(command -v python3 2>/dev/null || command -v python 2>/dev/null)
if [ -z "$PYBIN" ]; then
exit 0
fi
PARSED=$(echo "$INPUT" | "$PYBIN" -c "import sys,json
try:
d=json.load(sys.stdin)
tool=d.get('tool_name') or d.get('toolName') or ''
sid=d.get('session_id') or d.get('sessionId') or ''
resp=d.get('tool_response') or d.get('toolResponse') or {}
if isinstance(resp,str):
try: resp=json.loads(resp)
except Exception: resp={}
ok=isinstance(resp,dict) and resp.get('success') is True and 'error' not in resp
print(tool)
print(sid)
print('1' if ok else '0')
except Exception:
print('')
print('')
print('0')" 2>/dev/null)
TOOL=$(echo "$PARSED" | sed -n '1p')
SID=$(echo "$PARSED" | sed -n '2p')
OK=$(echo "$PARSED" | sed -n '3p')
case "$TOOL" in
*tapps_session_start) ;;
*) exit 0 ;;
esac
SID=$(printf '%s' "$INPUT" | sed -n 's/.*"session_id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1)
[ -z "$SID" ] && exit 0
if [ "$OK" != "1" ]; then
exit 0
fi
ROOT="${CLAUDE_PROJECT_DIR:-$PWD}"
mkdir -p "$ROOT/.tapps-mcp" 2>/dev/null
: > "$ROOT/.tapps-mcp/.session-start-done-$SID" 2>/dev/null
Expand Down
2 changes: 1 addition & 1 deletion .claude/hooks/tapps-post-validate.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# tapps-mcp-hook-version: 3.12.78
# tapps-mcp-hook-version: 3.12.83
# tapps-mcp-hook-content-sha: cca39db6
# TappsMCP PostToolUse hook (tapps_validate_changed)
# Reads the sidecar progress file and echoes a summary to the transcript.
Expand Down
174 changes: 171 additions & 3 deletions .claude/hooks/tapps-pre-bash.sh
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
#!/usr/bin/env bash
# tapps-mcp-hook-version: 3.12.78
# tapps-mcp-hook-content-sha: 0d76c000
# tapps-mcp-hook-version: 3.12.83
# tapps-mcp-hook-content-sha: da7d038c
# TappsMCP PreToolUse hook (Bash) - destructive command guard (opt-in)
# Blocks commands containing rm -rf, format c:, etc. Exit 2 = block, 0 = allow.
# TAP-6889: also blocks backgrounding, leaving the project dir, and a few
# suppression markers, but only when ORCHESTRATOR_GOAL_DISPATCH=1 (dispatched
# lanes) so interactive sessions are never affected.
INPUT=$(cat)
PYBIN=$(command -v python3 2>/dev/null || command -v python 2>/dev/null)
if [ -z "$PYBIN" ]; then
# TAP-1785: enforcement gate fails closed when python is unavailable.
ROOT="${CLAUDE_PROJECT_DIR:-$PWD}"
ROOT="${CLAUDE_PROJECT_DIR:-}"
if [ -z "$ROOT" ]; then
_common="$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null || true)"
if [ -n "$_common" ]; then
ROOT="$(cd "$_common/.." && pwd)"
else
ROOT="$PWD"
fi
fi
mkdir -p "$ROOT/.tapps-mcp" 2>/dev/null
echo "{\"ts\":\"$(date -u +%FT%TZ)\",\"hook\":\"tapps-pre-bash\",\"reason\":\"no_python\"}" \
>> "$ROOT/.tapps-mcp/.bypass-log.jsonl" 2>/dev/null
Expand Down Expand Up @@ -41,4 +52,161 @@ if [ "$BLOCK" = 1 ]; then
echo "TappsMCP: Blocked potentially destructive command." >&2
exit 2
fi
# TAP-6889: lane guard, gated on ORCHESTRATOR_GOAL_DISPATCH=1 so it only
# fires for dispatched lanes, never interactive sessions. Re-parses $INPUT
# independently of the $CMD extraction above (rather than sharing it) since
# a command can contain literal newlines and splitting combined stdout by
# line would corrupt it.
# TAP-6908: the "&" check used to be a trailing-suffix string test, which
# missed a background operator anywhere else in the command ("cmd & echo",
# a subshell "( cmd & )") and never looked inside a `bash -c '...'` payload.
# Both gaps are closed below.
if [ "$ORCHESTRATOR_GOAL_DISPATCH" = "1" ]; then
LANE_CHECK=$(echo "$INPUT" | "$PYBIN" -c "
import json, os, shlex, sys

BACKSLASH = chr(92)
SHELL_OPERATOR_CHARS = '();<>|&'
INTERPRETERS = ('bash', 'sh', 'zsh', 'dash')
SUPPRESSION_MARKERS = ('# noqa', '# type: ignore', '@pytest.mark.skip', 'xfail')


def _is_operator_token(tok):
return bool(tok) and all(c in SHELL_OPERATOR_CHARS for c in tok)


def _tokenize(cmd):
try:
lex = shlex.shlex(cmd, posix=True, punctuation_chars=True)
lex.whitespace_split = True
lex.commenters = ''
return list(lex)
except ValueError:
return []


def _has_background_amp(cmd):
# Character-level scan rather than another shlex pass: shlex's
# punctuation-run grouping merges an unquoted '&' with an immediately
# adjacent non-punctuation character into one token (e.g. 'cmd&;next'
# tokenizes as ['cmd', '&;', 'next'], and 'echo \$(pytest &)' produces
# a token '&)'), so a token == '&' equality check would silently miss
# both a mid-compound '&' glued to the next word and one inside a
# subshell / command substitution. This scan special-cases only '&&'
# (AND-list) and '>&' / '&>' (fd-merge redirects); every other
# unquoted bare '&' is treated as a background operator. Quote and
# escape tracking is a simplified approximation (backslash is a
# universal escape outside single quotes) -- deliberately not a full
# shell parser, per the smallest-robust-version constraint.
in_single = False
in_double = False
escaped = False
length = len(cmd)
i = 0
while i < length:
c = cmd[i]
if escaped:
escaped = False
elif c == BACKSLASH and not in_single:
escaped = True
elif c == chr(39) and not in_double:
in_single = not in_single
elif c == chr(34) and not in_single:
in_double = not in_double
elif c == '&' and not in_single and not in_double:
prev = cmd[i - 1] if i > 0 else ''
nxt = cmd[i + 1] if i + 1 < length else ''
if nxt == '&':
i += 2
continue
if prev in ('&', '>') or nxt == '>':
i += 1
continue
return True
i += 1
return False


def _cd_escapes_project(tokens, project_dir, project_real):
for idx, tok in enumerate(tokens):
if tok != 'cd':
continue
if idx + 1 >= len(tokens) or _is_operator_token(tokens[idx + 1]):
continue
target = tokens[idx + 1]
if target in ('-', '~'):
continue
if target.startswith('~/'):
target = os.path.expanduser('~') + target[1:]
if not os.path.isabs(target):
target = os.path.join(project_dir, target)
target_real = os.path.realpath(target)
if target_real != project_real and not target_real.startswith(project_real + os.sep):
return True
return False


def _check(cmd, project_dir, project_real, depth):
if _has_background_amp(cmd):
return 'background operator (&)'
tokens = _tokenize(cmd)
for word in ('nohup', 'disown', 'setsid'):
if word in tokens:
return word + ' command word'
if _cd_escapes_project(tokens, project_dir, project_real):
return 'cd outside project directory'
for marker in SUPPRESSION_MARKERS:
if marker in cmd:
return 'suppression marker'
# TAP-6908: recurse into a literal bash/sh/zsh/dash -c '...' payload so
# the same checks apply to a nested command string -- a quoted -c
# payload is no longer a blind spot. This only follows a literal
# string argument resolved by shlex: it does NOT expand shell
# variables (e.g. bash -c with \$CMD), command substitution, eval, or
# indirection through e.g. xargs -I{} bash -c. That remains an
# accepted, tested gap rather than a silent one -- see
# test_bash_c_variable_indirection_not_recursively_checked.
if depth < 4:
for idx, tok in enumerate(tokens):
if os.path.basename(tok) not in INTERPRETERS:
continue
for j in range(idx + 1, len(tokens)):
if tokens[j] == '-c':
if j + 1 < len(tokens):
nested = _check(tokens[j + 1], project_dir, project_real, depth + 1)
if nested:
return nested + ' (nested in ' + tok + ' -c)'
break
return None


try:
d = json.load(sys.stdin)
except Exception:
print('ALLOW')
sys.exit(0)
ti = d.get('tool_input', {}) or {}
cmd = ti.get('command', '') or ti.get('cmd', '')
if not cmd and isinstance(ti.get('args'), list):
cmd = ' '.join(str(a) for a in ti['args'])
if not isinstance(cmd, str):
cmd = ''
if ti.get('run_in_background') is True:
print('BLOCK:run_in_background tool_input flag')
sys.exit(0)
project_dir = os.environ.get('CLAUDE_PROJECT_DIR') or os.getcwd()
project_real = os.path.realpath(project_dir)
reason = _check(cmd, project_dir, project_real, 0)
if reason:
print('BLOCK:' + reason)
else:
print('ALLOW')
" 2>/dev/null)
case "$LANE_CHECK" in
BLOCK:*)
echo "TappsMCP: Blocked by lane guard - ${LANE_CHECK#BLOCK:} (ORCHESTRATOR_GOAL_DISPATCH=1)." >&2
exit 2
;;
esac
fi
exit 0
2 changes: 1 addition & 1 deletion .claude/hooks/tapps-pre-compact.sh
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# tapps-mcp-hook-version: 3.12.78
# tapps-mcp-hook-version: 3.12.83
# tapps-mcp-hook-content-sha: 1d39aaf5
# TappsMCP PreCompact hook (TAP-2017)
# Indexes pre-compaction session state in brain for post-compact rehydration.
Expand Down
14 changes: 11 additions & 3 deletions .claude/hooks/tapps-pre-linear-list.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env bash
# tapps-mcp-hook-version: 3.12.78
# tapps-mcp-hook-content-sha: 5862e6ab
# tapps-mcp-hook-version: 3.12.83
# tapps-mcp-hook-content-sha: a41e62ca
# TappsMCP PreToolUse hook — Linear cache-first read gate (TAP-1224)
# Gates raw mcp__plugin_linear_linear__list_issues calls behind a recent
# tapps_linear_snapshot_get sentinel for the same (team, project, state,
Expand Down Expand Up @@ -95,7 +95,15 @@ esac
if [ -z "$KEY" ]; then
exit 0
fi
ROOT="${CLAUDE_PROJECT_DIR:-$PWD}"
ROOT="${CLAUDE_PROJECT_DIR:-}"
if [ -z "$ROOT" ]; then
_common="$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null || true)"
if [ -n "$_common" ]; then
ROOT="$(cd "$_common/.." && pwd)"
else
ROOT="$PWD"
fi
fi
if [ "${TAPPS_LINEAR_SKIP_CACHE_GATE:-0}" = "1" ]; then
mkdir -p "$ROOT/.tapps-mcp" 2>/dev/null
echo "{\"ts\":\"$(date -u +%FT%TZ)\",\"bypass\":\"TAPPS_LINEAR_SKIP_CACHE_GATE\",\"key\":\"${KEY}\"}" \
Expand Down
Loading
Loading