fix: Set TERM and LANG for proper CLI rendering in tmux #156
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |