Skip to content

fix: Set TERM and LANG for proper CLI rendering in tmux #156

fix: Set TERM and LANG for proper CLI rendering in tmux

fix: Set TERM and LANG for proper CLI rendering in tmux #156

Workflow file for this run

name: Claude Code
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, reopened]
pull_request_review:
types: [submitted]
# SECURITY NOTE: This workflow grants Claude Code access to secrets and write permissions.
# Authorization checks are implemented to prevent prompt injection from untrusted users.
# Only repository collaborators, members, and owners can trigger Claude.
env:
# Note: must be lowercase for Docker compatibility
DEVCONTAINER_IMAGE: ghcr.io/nickborgersprobably/hide-my-list-devcontainer
jobs:
# Security gate: Check if the actor is authorized to trigger Claude
# This prevents prompt injection attacks from untrusted external users
authorize:
runs-on: ubuntu-latest
outputs:
authorized: ${{ steps.check-auth.outputs.authorized }}
steps:
- name: Check authorization
id: check-auth
env:
GH_TOKEN: ${{ github.token }}
run: |
# Get the author association based on event type
EVENT_NAME="${{ github.event_name }}"
case "$EVENT_NAME" in
issue_comment)
AUTHOR_ASSOC="${{ github.event.comment.author_association }}"
ACTOR="${{ github.event.comment.user.login }}"
;;
pull_request_review_comment)
AUTHOR_ASSOC="${{ github.event.comment.author_association }}"
ACTOR="${{ github.event.comment.user.login }}"
;;
pull_request_review)
AUTHOR_ASSOC="${{ github.event.review.author_association }}"
ACTOR="${{ github.event.review.user.login }}"
;;
issues)
AUTHOR_ASSOC="${{ github.event.issue.author_association }}"
ACTOR="${{ github.event.issue.user.login }}"
;;
*)
echo "Unknown event type: $EVENT_NAME"
echo "authorized=false" >> $GITHUB_OUTPUT
exit 0
;;
esac
echo "Event: $EVENT_NAME"
echo "Actor: $ACTOR"
echo "Author association: $AUTHOR_ASSOC"
# Allow: OWNER, MEMBER, COLLABORATOR
# Deny: CONTRIBUTOR, FIRST_TIMER, FIRST_TIME_CONTRIBUTOR, MANNEQUIN, NONE
case "$AUTHOR_ASSOC" in
OWNER|MEMBER|COLLABORATOR)
echo "Authorized: $ACTOR is a $AUTHOR_ASSOC"
echo "authorized=true" >> $GITHUB_OUTPUT
;;
*)
echo "Not authorized: $ACTOR is a $AUTHOR_ASSOC"
echo "External users cannot trigger Claude Code workflows."
echo "This is a security measure to prevent prompt injection attacks."
echo "authorized=false" >> $GITHUB_OUTPUT
;;
esac
# Build and cache devcontainer image first
build-devcontainer:
needs: authorize
if: needs.authorize.outputs.authorized == 'true'
runs-on: [self-hosted, homelab]
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push devcontainer
uses: devcontainers/ci@v0.3
with:
imageName: ${{ env.DEVCONTAINER_IMAGE }}
cacheFrom: ${{ env.DEVCONTAINER_IMAGE }}
push: always
claude:
needs: [authorize, build-devcontainer]
if: |
needs.authorize.outputs.authorized == 'true' &&
((github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))))
runs-on: [self-hosted, homelab]
permissions:
contents: write
pull-requests: write
issues: write
id-token: write
actions: read
packages: read
steps:
- name: Add eyes reaction to acknowledge comment
env:
GH_TOKEN: ${{ github.token }}
run: |
if [ "${{ github.event_name }}" = "issue_comment" ]; then
gh api repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions \
-f content='eyes' || echo "Warning: reaction failed"
elif [ "${{ github.event_name }}" = "pull_request_review_comment" ]; then
gh api repos/${{ github.repository }}/pulls/comments/${{ github.event.comment.id }}/reactions \
-f content='eyes' || echo "Warning: reaction failed"
elif [ "${{ github.event_name }}" = "pull_request_review" ]; then
echo "Note: GitHub API does not support reactions on PR reviews"
fi
- name: Get PR context
id: pr-context
env:
GH_TOKEN: ${{ github.token }}
run: |
# Determine if this is a PR comment and get PR details
IS_PR="false"
PR_HEAD_REF=""
PR_NUMBER=""
if [ "${{ github.event_name }}" = "pull_request_review_comment" ]; then
IS_PR="true"
PR_NUMBER="${{ github.event.pull_request.number }}"
PR_HEAD_REF="${{ github.event.pull_request.head.ref }}"
elif [ "${{ github.event_name }}" = "pull_request_review" ]; then
IS_PR="true"
PR_NUMBER="${{ github.event.pull_request.number }}"
PR_HEAD_REF="${{ github.event.pull_request.head.ref }}"
elif [ "${{ github.event_name }}" = "issue_comment" ]; then
# Check if the issue is actually a PR
if [ -n "${{ github.event.issue.pull_request }}" ]; then
IS_PR="true"
PR_NUMBER="${{ github.event.issue.number }}"
# Fetch PR details to get head ref
PR_HEAD_REF=$(gh api repos/${{ github.repository }}/pulls/${PR_NUMBER} --jq '.head.ref')
fi
fi
echo "is_pr=$IS_PR" >> $GITHUB_OUTPUT
echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT
echo "pr_head_ref=$PR_HEAD_REF" >> $GITHUB_OUTPUT
echo "Detected: IS_PR=$IS_PR, PR_NUMBER=$PR_NUMBER, PR_HEAD_REF=$PR_HEAD_REF"
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.WORKFLOW_PAT }} # PAT required to push workflow file changes
ref: ${{ steps.pr-context.outputs.is_pr == 'true' && steps.pr-context.outputs.pr_head_ref || github.ref }}
- name: Extract comment body
id: comment
env:
EVENT_NAME: ${{ github.event_name }}
COMMENT_BODY: ${{ github.event.comment.body }}
REVIEW_BODY: ${{ github.event.review.body }}
ISSUE_BODY: ${{ github.event.issue.body }}
run: |
# Use environment variables to avoid shell escaping issues with special characters
echo "body<<EOF" >> $GITHUB_OUTPUT
if [ "$EVENT_NAME" = "issue_comment" ]; then
echo "$COMMENT_BODY" >> $GITHUB_OUTPUT
elif [ "$EVENT_NAME" = "pull_request_review_comment" ]; then
echo "$COMMENT_BODY" >> $GITHUB_OUTPUT
elif [ "$EVENT_NAME" = "pull_request_review" ]; then
echo "$REVIEW_BODY" >> $GITHUB_OUTPUT
else
echo "$ISSUE_BODY" >> $GITHUB_OUTPUT
fi
echo "EOF" >> $GITHUB_OUTPUT
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Note: GH_TOKEN uses WORKFLOW_PAT because PRs created with GITHUB_TOKEN don't trigger workflows
- name: Run Claude in devcontainer
uses: devcontainers/ci@v0.3
with:
imageName: ${{ env.DEVCONTAINER_IMAGE }}
cacheFrom: ${{ env.DEVCONTAINER_IMAGE }}
push: never
env: |
CLAUDE_CODE_OAUTH_TOKEN=${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
GH_TOKEN=${{ secrets.WORKFLOW_PAT }}
COMMENT_BODY=${{ steps.comment.outputs.body }}
ISSUE_NUMBER=${{ github.event.issue.number || github.event.pull_request.number }}
REPO=${{ github.repository }}
CLAUDE_CONFIG_DIR=/tmp/.claude-session
IS_PR=${{ steps.pr-context.outputs.is_pr }}
PR_HEAD_REF=${{ steps.pr-context.outputs.pr_head_ref }}
runCmd: |
# Debug: verify auth token propagation
echo "CLAUDE_CODE_OAUTH_TOKEN is set: $([ -n "$CLAUDE_CODE_OAUTH_TOKEN" ] && echo 'YES (length: '${#CLAUDE_CODE_OAUTH_TOKEN}')' || echo 'NO')"
# Fail fast if auth token is missing
if [ -z "$CLAUDE_CODE_OAUTH_TOKEN" ]; then
echo "ERROR: CLAUDE_CODE_OAUTH_TOKEN is empty inside container."
exit 1
fi
# Ensure Claude onboarding is complete
if [ ! -f ~/.claude.json ]; then
echo '{"hasCompletedOnboarding": true}' > ~/.claude.json
echo "Created ~/.claude.json"
fi
# Run Claude Code inside the devcontainer with full tool access
# Use < /dev/null to prevent hanging on TTY input
# Use tee to capture output for artifact upload
# Use --verbose and --output-format stream-json for complete conversation logs
mkdir -p "${CLAUDE_CONFIG_DIR}"
# Build the prompt based on whether this is a PR or issue comment
# Instead of fetching all context inline, instruct Claude to read it via gh CLI.
# This avoids pagination limits, prompt size bloat, and comment duplication.
if [ "$IS_PR" = "true" ]; then
PROMPT="You are an autonomous agent responding to a request on PR #${ISSUE_NUMBER} in ${REPO}.
The user said: ${COMMENT_BODY}
YOUR TASK: IMPLEMENT the requested changes. Do NOT just provide review comments or suggestions.
WORKFLOW:
1. Read the user's request carefully
2. If they reference another comment, fetch it: gh api repos/${REPO}/issues/${ISSUE_NUMBER}/comments
3. Understand what changes are needed
4. Implement the changes in the code
5. Run \`shellcheck scripts/*.sh\` and \`yamllint .github/workflows/*.yml\` to verify your changes
6. Commit your changes with a descriptive message
7. Push to the PR branch: git push origin ${PR_HEAD_REF}
8. Post a summary comment: gh pr comment ${ISSUE_NUMBER} --body 'YOUR_SUMMARY'
IMPORTANT RULES FOR THIS REPOSITORY:
- This is an OpenClaw agent project, not a compiled application
- Use \`shellcheck scripts/*.sh\` for shell script linting
- Use \`yamllint .github/workflows/*.yml\` for workflow validation
- Check documentation links are not broken
- Reference docs/architecture.md for system design context
- Never use \`git push --no-verify\`
You are on branch ${PR_HEAD_REF}. Make the changes, commit, and push them."
else
PROMPT="You are responding to a request in ${REPO} issue #${ISSUE_NUMBER}.
The user said: ${COMMENT_BODY}
WORKFLOW:
1. Read the user's request carefully
2. Fetch the full issue for context: gh api repos/${REPO}/issues/${ISSUE_NUMBER}
3. If needed, fetch issue comments: gh api repos/${REPO}/issues/${ISSUE_NUMBER}/comments
4. Implement the requested changes
5. Run \`shellcheck scripts/*.sh\` and \`yamllint .github/workflows/*.yml\` to verify your changes
6. Create a branch, commit your changes, and open a PR
IMPORTANT RULES FOR THIS REPOSITORY:
- This is an OpenClaw agent project, not a compiled application
- Use \`shellcheck scripts/*.sh\` for shell script linting
- Use \`yamllint .github/workflows/*.yml\` for workflow validation
- Check documentation links are not broken
- Reference docs/architecture.md for system design context
- Never use \`git push --no-verify\`
Complete the user's request. If you make changes, create a branch, commit them, and open a PR.
BEFORE creating a new PR, check if one already exists for this issue:
gh pr list --state open --search 'Resolves #${ISSUE_NUMBER} OR Fixes #${ISSUE_NUMBER} OR Closes #${ISSUE_NUMBER}'
If a PR already exists, push changes to that PR's branch instead of creating a new one.
Reply to the user using: gh issue comment ${ISSUE_NUMBER} --body 'YOUR_RESPONSE'"
fi
claude --print \
--verbose \
--output-format stream-json \
--model opus \
--dangerously-skip-permissions \
--max-turns 400 \
"$PROMPT" < /dev/null 2>&1 | tee /tmp/claude-conversation.jsonl
- name: Upload Claude conversation log
uses: actions/upload-artifact@v4
if: always()
continue-on-error: true
with:
name: claude-conversation-log
path: /tmp/claude-conversation.jsonl
retention-days: 7
- name: Upload Claude session data
uses: actions/upload-artifact@v4
if: always()
continue-on-error: true
with:
name: claude-session-data
path: /tmp/.claude-session/
retention-days: 7
if-no-files-found: ignore
# Auto-resolve new issues by opening a PR
resolve-issue:
needs: [authorize, build-devcontainer]
if: |
needs.authorize.outputs.authorized == 'true' &&
github.event_name == 'issues' && (github.event.action == 'opened' || github.event.action == 'reopened')
runs-on: [self-hosted, homelab]
permissions:
contents: write
pull-requests: write
issues: write
id-token: write
packages: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.WORKFLOW_PAT }} # PAT required to push workflow file changes
- name: Check for existing PR
id: check-existing
env:
GH_TOKEN: ${{ github.token }}
run: |
ISSUE_NUM=${{ github.event.issue.number }}
# Search for open PRs whose body contains "Resolves #N", "Fixes #N", or "Closes #N"
EXISTING=$(gh pr list --state open --search "Resolves #${ISSUE_NUM} OR Fixes #${ISSUE_NUM} OR Closes #${ISSUE_NUM}" --json number --jq '.[0].number // empty')
if [ -n "$EXISTING" ]; then
echo "PR #${EXISTING} already targets issue #${ISSUE_NUM}, skipping"
echo "skip=true" >> $GITHUB_OUTPUT
else
echo "No existing PR found for issue #${ISSUE_NUM}"
echo "skip=false" >> $GITHUB_OUTPUT
fi
- name: Label issue as claude-started
if: steps.check-existing.outputs.skip != 'true'
env:
GH_TOKEN: ${{ github.token }}
run: |
# Create the label if it doesn't exist (ignore error if it does)
gh label create "claude-started" --color "7057ff" --description "Claude Code is working on this issue" --repo ${{ github.repository }} 2>/dev/null || true
# Add the label to the issue
gh issue edit ${{ github.event.issue.number }} --add-label "claude-started" --repo ${{ github.repository }}
- name: Log in to GHCR
if: steps.check-existing.outputs.skip != 'true'
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Note: GH_TOKEN uses WORKFLOW_PAT because PRs created with GITHUB_TOKEN don't trigger workflows
- name: Resolve issue in devcontainer
if: steps.check-existing.outputs.skip != 'true'
uses: devcontainers/ci@v0.3
with:
imageName: ${{ env.DEVCONTAINER_IMAGE }}
cacheFrom: ${{ env.DEVCONTAINER_IMAGE }}
push: never
env: |
CLAUDE_CODE_OAUTH_TOKEN=${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
GH_TOKEN=${{ secrets.WORKFLOW_PAT }}
ISSUE_NUMBER=${{ github.event.issue.number }}
ISSUE_TITLE=${{ github.event.issue.title }}
REPO=${{ github.repository }}
CLAUDE_CONFIG_DIR=/tmp/.claude-session
runCmd: |
echo "CLAUDE_CODE_OAUTH_TOKEN is set: $([ -n "$CLAUDE_CODE_OAUTH_TOKEN" ] && echo 'YES (length: '${#CLAUDE_CODE_OAUTH_TOKEN}')' || echo 'NO')"
if [ -z "$CLAUDE_CODE_OAUTH_TOKEN" ]; then
echo "ERROR: CLAUDE_CODE_OAUTH_TOKEN is empty inside container."
exit 1
fi
if [ ! -f ~/.claude.json ]; then
echo '{"hasCompletedOnboarding": true}' > ~/.claude.json
fi
# Use < /dev/null to prevent hanging on TTY input
# Use tee to capture output for artifact upload
# Use --verbose and --output-format stream-json for complete conversation logs
mkdir -p "${CLAUDE_CONFIG_DIR}"
claude --print \
--verbose \
--output-format stream-json \
--model opus \
--dangerously-skip-permissions \
--max-turns 600 \
"You are an autonomous agent tasked with resolving GitHub issue #${ISSUE_NUMBER} in ${REPO}.
ISSUE TITLE: ${ISSUE_TITLE}
YOUR TASK:
1. Fetch the full issue for context: gh api repos/${REPO}/issues/${ISSUE_NUMBER}
2. If needed, fetch issue comments: gh api repos/${REPO}/issues/${ISSUE_NUMBER}/comments
3. Analyze the issue and understand what needs to be done
4. Explore the codebase to find relevant files
5. Implement the fix or feature
6. Run \`shellcheck scripts/*.sh\` and \`yamllint .github/workflows/*.yml\` to verify your changes
7. Create a branch, commit your changes, and open a PR
IMPORTANT RULES:
- This is an OpenClaw agent project, not a compiled application
- Use \`shellcheck scripts/*.sh\` for shell script linting
- Use \`yamllint .github/workflows/*.yml\` for workflow validation
- Check documentation links are not broken
- Reference docs/architecture.md for system design context
- Never use \`git push --no-verify\`
BRANCH NAMING: Use \`claude/issue-${ISSUE_NUMBER}\` as the branch name.
PR CREATION:
gh pr create --title '<brief description of what this PR accomplishes>' \\
--assignee NickBorgers \\
--body 'Resolves #${ISSUE_NUMBER}
## Summary
<describe what you changed>
## Test Plan
<how to verify the fix>
Generated with Claude Code' \\
--head claude/issue-${ISSUE_NUMBER}
If you cannot resolve the issue (unclear requirements, needs human decision, etc.),
comment on the issue explaining what clarification is needed:
gh issue comment ${ISSUE_NUMBER} --body 'YOUR_EXPLANATION'" < /dev/null 2>&1 | tee /tmp/resolve-issue-conversation.jsonl
- name: Upload issue resolution conversation log
uses: actions/upload-artifact@v4
if: always()
continue-on-error: true
with:
name: resolve-issue-conversation-log
path: /tmp/resolve-issue-conversation.jsonl
retention-days: 7
- name: Upload Claude session data
uses: actions/upload-artifact@v4
if: always()
continue-on-error: true
with:
name: resolve-issue-session-data
path: /tmp/.claude-session/
retention-days: 7
if-no-files-found: ignore