.claude/skills/tenancy-enforcer.md/SKILL.md
Use this skill when writing MongoDB queries, repository methods, or service logic in Aegis. It enforces strict multi-tenancy by ensuring communityId is always included.
npx skillsauth add muhammadcaeed/aegis tenancy-enforcerInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Every database query MUST include communityId in the filter.
No exceptions. Cross-community access is a security breach.
// Controller extracts from token
@Get()
async findAll(@CurrentUser() user: JwtPayload) {
return this.service.findAll(user.communityId);
}
// Service passes to repository
async findAll(communityId: string) {
return this.repository.findAll(communityId);
}
// Repository includes in query
async findAll(communityId: string) {
return this.model.find({
communityId: new Types.ObjectId(communityId),
});
}
// NEVER DO THIS
@Post()
async create(@Body() dto: CreateDto) {
// dto.communityId could be forged by attacker
return this.service.create(dto.communityId, dto);
}
// Always require communityId as parameter
async findById(id: string, communityId: string): Promise<Document | null> {
return this.model.findOne({
_id: new Types.ObjectId(id),
communityId: new Types.ObjectId(communityId), // REQUIRED
});
}
async findByHousehold(householdId: string, communityId: string): Promise<Document[]> {
return this.model.find({
householdId: new Types.ObjectId(householdId),
communityId: new Types.ObjectId(communityId), // REQUIRED
});
}
async updateById(
id: string,
communityId: string,
data: UpdateDto,
): Promise<Document | null> {
return this.model.findOneAndUpdate(
{
_id: new Types.ObjectId(id),
communityId: new Types.ObjectId(communityId), // REQUIRED in filter
},
{ $set: data },
{ new: true },
);
}
async deleteById(id: string, communityId: string): Promise<boolean> {
const result = await this.model.deleteOne({
_id: new Types.ObjectId(id),
communityId: new Types.ObjectId(communityId), // REQUIRED
});
return result.deletedCount > 0;
}
// WRONG - No communityId filter
async findAll(): Promise<Document[]> {
return this.model.find({}); // SECURITY BREACH
}
// WRONG - ID-only lookup
async findById(id: string): Promise<Document | null> {
return this.model.findById(id); // Can access ANY community's data
}
// WRONG - communityId should never be optional
async findById(id: string, communityId?: string): Promise<Document | null> {
const filter: any = { _id: new Types.ObjectId(id) };
if (communityId) {
filter.communityId = new Types.ObjectId(communityId);
}
return this.model.findOne(filter); // Dangerous when omitted
}
// WRONG - Request body can be forged
@Post()
async create(@Body() dto: CreatePaymentDto) {
return this.paymentService.create({
...dto,
communityId: dto.communityId, // Attacker can set any communityId
});
}
// CORRECT - Use JWT context
@Post()
async create(
@Body() dto: CreatePaymentDto,
@CurrentUser() user: JwtPayload,
) {
return this.paymentService.create({
...dto,
communityId: user.communityId, // From authenticated token
});
}
All compound indexes MUST have communityId as the first field for query efficiency and logical isolation.
// CORRECT - communityId first
@Schema()
export class Payment {
// ...
}
PaymentSchema.index({ communityId: 1, householdId: 1 });
PaymentSchema.index({ communityId: 1, state: 1 });
PaymentSchema.index({ communityId: 1, householdId: 1, billingPeriodStart: 1 }, { unique: true });
// WRONG - communityId not first
PaymentSchema.index({ householdId: 1, communityId: 1 }); // Query won't use index efficiently
PaymentSchema.index({ state: 1 }); // Global index, no tenant isolation
// CORRECT - $match with communityId as first stage
async aggregateByHousehold(communityId: string) {
return this.model.aggregate([
{
$match: {
communityId: new Types.ObjectId(communityId), // FIRST stage
},
},
{
$group: {
_id: '$householdId',
total: { $sum: '$amount' },
},
},
]);
}
// WRONG - Missing communityId or not first
async aggregateAll() {
return this.model.aggregate([
{ $group: { _id: '$householdId', total: { $sum: '$amount' } } }, // No tenant filter
]);
}
Services should always require communityId from controllers, never have default values or lookups.
// CORRECT - Explicit communityId parameter
@Injectable()
export class PaymentService {
async findByHousehold(householdId: string, communityId: string) {
return this.repository.findByHousehold(householdId, communityId);
}
}
// WRONG - Fetching communityId from related entity
@Injectable()
export class PaymentService {
async findByHousehold(householdId: string) {
const household = await this.householdService.findById(householdId);
// What if household doesn't exist? What if wrong community?
return this.repository.findByHousehold(householdId, household.communityId);
}
}
The Community collection itself does not have a communityId field - it IS the tenant root. When querying communities:
// Community queries use _id directly (admin operations only)
async findCommunityById(id: string): Promise<Community | null> {
return this.communityModel.findById(id);
}
Before committing any repository/service code:
find() call includes communityId in filterfindOne() call includes communityId in filterupdateOne()/updateMany() includes communityId in filterdeleteOne()/deleteMany() includes communityId in filter$match with communityId as first stagecommunityId comes from JWT/controller, not request bodycommunityId parametercommunityId as first field in compound indexestools
Use this skill when modifying payment states, implementing verification flows, or calculating late statuses in the Aegis platform.
development
Mandatory backend module architecture and implementation rules for the Aegis system
development
Maintainer-only workflow for handling GitHub Secret Scanning alerts on OpenClaw. Use when Codex needs to triage, redact, clean up, and resolve secret leakage found in issue comments, issue bodies, PR comments, or other GitHub content.
development
Maintainer workflow for OpenClaw releases, prereleases, changelog release notes, and publish validation. Use when Codex needs to prepare or verify stable or beta release steps, align version naming, assemble release notes, check release auth requirements, or validate publish-time commands and artifacts.