Skip to content

Commit 4d59873

Browse files
committed
fix(container): harden task-tracker locking, quality-gate ordering, and CDP endpoint
- task-tracker.py: add fcntl-based file locking with retry (3 attempts, 100ms) to prevent race conditions on concurrent TaskCreated/TaskCompleted hooks; add event type validation for early exit on unknown events - quality-gate.py: move temp file deletion after block response output to ensure the block is sent even if cleanup fails; add session_id missing warning to stderr - devcontainer.json: replace hardcoded 192.168.65.254 CDP endpoint with host.docker.internal for dynamic resolution across Docker environments - install.sh: replace echo with heredoc for host Chrome usage examples so users can copy-paste directly without escaping issues
1 parent 7ba933e commit 4d59873

4 files changed

Lines changed: 78 additions & 43 deletions

File tree

container/.devcontainer/devcontainer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
"CLAUDE_CONFIG_DIR": "/home/vscode/.claude",
2929
"GH_CONFIG_DIR": "/workspaces/.gh",
3030
"TMPDIR": "/workspaces/.tmp",
31-
"HERMES_CDP_ENDPOINT": "http://192.168.65.254:9223",
31+
"HERMES_CDP_ENDPOINT": "http://host.docker.internal:9223",
3232
"TERM": "${localEnv:TERM:xterm-256color}",
3333
"COLORTERM": "truecolor",
3434
"CLAUDECODE": null

container/.devcontainer/features/agent-browser/install.sh

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ echo " agent-browser close # Close browser"
7878
echo ""
7979
echo "Host Chrome connection (if container browser insufficient):"
8080
echo " # Windows: run .devcontainer\\scripts\\start-hermes-chrome.ps1 on the host"
81-
echo " CDP_HOST=\$(getent ahostsv4 host.docker.internal | awk 'NR==1 {print \$1}')"
82-
echo " curl http://\$CDP_HOST:9223/json/version"
83-
echo " agent-browser connect \$CDP_HOST:9223"
81+
cat <<'USAGE'
82+
CDP_HOST=$(getent ahostsv4 host.docker.internal | awk 'NR==1 {print $1}')
83+
curl http://$CDP_HOST:9223/json/version
84+
agent-browser connect $CDP_HOST:9223
85+
USAGE

container/.devcontainer/plugins/devs-marketplace/plugins/auto-code-quality/scripts/quality-gate.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ def main():
3636

3737
session_id = input_data.get("session_id", "")
3838
if not session_id:
39+
print("quality-gate: session_id missing from hook input", file=sys.stderr)
3940
sys.exit(0)
4041

4142
# Skip if background tasks are still running
@@ -78,13 +79,6 @@ def main():
7879
sys.exit(0)
7980

8081
# Block and tell Claude to run /cq
81-
# Delete temp files so the NEXT stop (after /cq runs) exits clean
82-
for prefix in ("claude-cq-edited", "claude-cq-lint"):
83-
try:
84-
os.unlink(f"/tmp/{prefix}-{session_id}")
85-
except OSError:
86-
pass
87-
8882
file_list = "\n".join(f" - {p}" for p in paths)
8983
json.dump(
9084
{
@@ -97,6 +91,14 @@ def main():
9791
},
9892
sys.stdout,
9993
)
94+
95+
# Delete temp files so the NEXT stop (after /cq runs) exits clean
96+
for prefix in ("claude-cq-edited", "claude-cq-lint"):
97+
try:
98+
os.unlink(f"/tmp/{prefix}-{session_id}")
99+
except OSError:
100+
pass
101+
100102
sys.exit(0)
101103

102104

container/.devcontainer/plugins/devs-marketplace/plugins/auto-code-quality/scripts/task-tracker.py

Lines changed: 63 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@
99
Always exits 0.
1010
"""
1111

12+
import fcntl
1213
import json
1314
import os
1415
import sys
16+
import time
1517

1618
# Hook gate — check ~/.claude/disabled-hooks.json
1719
_dh = os.path.join(os.path.expanduser("~"), ".claude", "disabled-hooks.json")
@@ -23,6 +25,62 @@
2325
sys.exit(0)
2426

2527

28+
_MAX_RETRIES = 3
29+
_RETRY_DELAY = 0.1 # 100ms
30+
31+
32+
def _locked_append(path: str, data: str) -> None:
33+
"""Append data to file under an exclusive lock."""
34+
for attempt in range(_MAX_RETRIES):
35+
try:
36+
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o644)
37+
try:
38+
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
39+
os.write(fd, data.encode())
40+
return
41+
finally:
42+
os.close(fd)
43+
except BlockingIOError:
44+
if attempt < _MAX_RETRIES - 1:
45+
time.sleep(_RETRY_DELAY)
46+
except OSError:
47+
return
48+
49+
50+
def _locked_remove(path: str, task_id: str) -> None:
51+
"""Remove a task ID from the file under an exclusive lock."""
52+
for attempt in range(_MAX_RETRIES):
53+
try:
54+
with open(path, "r+") as f:
55+
fcntl.flock(f.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
56+
lines = [line.strip() for line in f if line.strip()]
57+
58+
try:
59+
lines.remove(task_id)
60+
except ValueError:
61+
return # Task ID not found — may have been cleaned up
62+
63+
if lines:
64+
f.seek(0)
65+
f.truncate()
66+
f.write("\n".join(lines) + "\n")
67+
else:
68+
# No more active tasks — remove file after releasing lock
69+
f.close()
70+
try:
71+
os.unlink(path)
72+
except OSError:
73+
pass
74+
return
75+
except FileNotFoundError:
76+
return
77+
except BlockingIOError:
78+
if attempt < _MAX_RETRIES - 1:
79+
time.sleep(_RETRY_DELAY)
80+
except OSError:
81+
return
82+
83+
2684
def main():
2785
try:
2886
input_data = json.load(sys.stdin)
@@ -34,43 +92,16 @@ def main():
3492
sys.exit(0)
3593

3694
event = input_data.get("hook_event_name", "")
95+
if event not in ("TaskCreated", "TaskCompleted"):
96+
sys.exit(0)
97+
3798
task_id = input_data.get("task_id", "") or input_data.get("id", "") or "unknown"
3899
tasks_file = f"/tmp/claude-active-tasks-{session_id}"
39100

40101
if event == "TaskCreated":
41-
try:
42-
with open(tasks_file, "a") as f:
43-
f.write(task_id + "\n")
44-
except OSError:
45-
pass
46-
102+
_locked_append(tasks_file, task_id + "\n")
47103
elif event == "TaskCompleted":
48-
try:
49-
with open(tasks_file) as f:
50-
lines = [line.strip() for line in f if line.strip()]
51-
except FileNotFoundError:
52-
sys.exit(0)
53-
except OSError:
54-
sys.exit(0)
55-
56-
# Remove first occurrence of this task ID
57-
try:
58-
lines.remove(task_id)
59-
except ValueError:
60-
pass # Task ID not found — may have been cleaned up
61-
62-
if lines:
63-
try:
64-
with open(tasks_file, "w") as f:
65-
f.write("\n".join(lines) + "\n")
66-
except OSError:
67-
pass
68-
else:
69-
# No more active tasks — clean up
70-
try:
71-
os.unlink(tasks_file)
72-
except OSError:
73-
pass
104+
_locked_remove(tasks_file, task_id)
74105

75106
sys.exit(0)
76107

0 commit comments

Comments
 (0)